Значения не загружаются во время начальной загрузки наблюдаемых значений - PullRequest
1 голос
/ 10 апреля 2019

В приложении Angular я хочу динамически загружать меню из базы данных (проект .NEt Core).Для этого я создал API-сервис для получения данных в формате json.Чтобы запросить этот формат json у внешнего интерфейса, я использую сервис в workout.service.ts -файле (я пробовал его с и без наблюдаемого, но получил тот же результат).

Для загрузки меню, которое я использовалmenu.service.ts -файл, в котором функция getVerticalMenuItems() используется для загрузки вертикальных меню, которые не работают во время инициализации;

Здесь я использую метод resultmenu.push для преобразования данных в следующий формат:

export const verticalMenuItems = [
  new Menu(1, 'Dashboard', '/', null, 'dashboard', null, false, 0),
  new Menu(100, 'Action', '/actions', null, 'extension', null, false, 0)]

В вертикальной загрузке меню мы используем vertical-menu.component.ts (я предполагаю, что здесь возникает проблема, которая всегда показывает значение Array [0] = null)

menu.modal.ts

export class Menu {
    constructor(public id: number,
                public title: string,
                public routerLink: string,
                public href: string,
                public icon: string,
                public target: string,
                public hasSubMenu: boolean,
                public parentId: number) { }
} 


workout.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import 'rxjs/Rx';
import { User } from './models/user.model';
import { Observable } from 'rxjs/Rx';
import { Menu } from './theme/components/menu/menu.model';

@Injectable()
export class WorkoutService {


private headers: HttpHeaders;

  private menuUrl: string = 'https://localhost:44355/api/Menus';

constructor(private http: HttpClient) {
this.headers= new HttpHeaders({'Content-Type':'application/json; charset=utf-8});}

  // Get Menus
  public getMenus() {return this.http.get(this.menuUrl, { headers: this.headers }).map((response: Response) => {     
      return response;
    });

  }


}

menu.service.ts

@Injectable()
export class MenuService {
  my_menu: Array<Menu>;

  constructor(private location:Location,
              private router: Router,
              private workoutService: WorkoutService) { } 



// for vertical Menu items loading
 public getVerticalMenuItems(): Array<Menu> {  
    const resultMenu: Array<Menu>=[];       
    this.workoutService.getMenus()
       .subscribe(
       (data:any) => {       
        data.forEach(i => {
          resultMenu.push(new Menu(i.id, i.title, i.routerLink, i.href, i.icon, i.target, i.hasSubMenu, i.parentId))
        })
        console.log(resultMenu);
      }
    );

    console.log(resultMenu);
    console.log(verticalMenuItems);
    return resultMenu;
    //return verticalMenuItems; -- NOTE: old working code taken from Gradus theme templates ;(working code)

  }

// working code
  public getHorizontalMenuItems(): Array<Menu> {
      return horizontalMenuItems;
  }


public expandActiveSubMenu(menu: Observable<Menu[]>){
      let url = this.location.path();
      let routerLink = url; // url.substring(1, url.length);
      let activeMenuItem = menu.map(items=>items.filter(item => item.routerLink === routerLink));
      if(activeMenuItem[0]){
        let menuItem = activeMenuItem[0];
        while (menuItem.parentId != 0){  
          let parentMenuItem = menu.map(items=>items.filter(item => item.id == menuItem.parentId)[0]);
          menuItem = parentMenuItem;
          this.toggleMenuItem(menuItem.id);
        }
      }
  }

  public toggleMenuItem(menuId){
    let menuItem = document.getElementById('menu-item-'+menuId);
    let subMenu = document.getElementById('sub-menu-'+menuId);  
    if(subMenu){
      if(subMenu.classList.contains('show')){
        subMenu.classList.remove('show');
        menuItem.classList.remove('expanded');
      }
      else{
        subMenu.classList.add('show');
        menuItem.classList.add('expanded');
      }      
    }
  }

  public closeOtherSubMenus(menu: Array<Menu>, menuId) {
    debugger;
    let currentMenuItem = menu.filter(item => item.id == menuId)[0]; 
    if(currentMenuItem.parentId == 0 && !currentMenuItem.target){
      menu.forEach(item => {
        if(item.id != menuId){
          let subMenu = document.getElementById('sub-menu-'+item.id);
          let menuItem = document.getElementById('menu-item-'+item.id);
          if(subMenu){
            if(subMenu.classList.contains('show')){
              subMenu.classList.remove('show');
              menuItem.classList.remove('expanded');
            }              
          } 
        }
      });
    }


vertical-menu.component.ts


export class VerticalMenuComponent implements OnInit {
  @Input() resultMenu: Array<Menu> = [];

  /***************************************
  * MOVED THE NEW INPUTS TO HERE
  ****************************************/
  @Input()
  set menuItems(items: Menu[]) {
    this._menuItemsLoaded$.next(items);
  }
  _menuItemsLoaded$: ReplaySubject<Menu[]> = new ReplaySubject<Menu[]>(1);

  // We must save the received items and also emit it
  @Input() 
  set menuParentId(items: number) {
    this._menuParentIdLoaded$.next(items);
  }
  _menuParentIdLoaded$: ReplaySubject<number> = 
  new ReplaySubject<number>(1);

  /***************************************
  * END OF NEW INPUTS
  ****************************************/

  @Output() onClickMenuItem: EventEmitter<any> = new EventEmitter<any>();
  parentMenu: Array<any>;
  public settings: Settings;

  constructor(public appSettings: AppSettings, public menuService: MenuService, public router: Router) {
    this.settings = this.appSettings.settings;
  }

  ngOnInit() {
  /***************************************
   * FIXED THE FILTERS INSIDE THE PIPES
   ****************************************/
    combineLatest(
      this._menuItemsLoaded$.pipe(
            filter(Boolean), 
            filter((i) => !!i.length), 
            debounceTime(300)),
      this._menuParentIdLoaded$.pipe(
            // for _menuParentId (exclusively) we cannot use 
            // filter(Boolean) here, or the 0 values will be blocked
            filter((i) => i !== null && i !== undefined),
            debounceTime(300)),
    ).subscribe(([menuItems, parentId]) => 
      this.parentMenu = menuItems.filter(item => item.parentId == parentId)
    );
  }

  // as we're using some subjects, we must finalize
  // them in case this component is eventually destroyed
  ngOnDestroy() {
    if (this._menuItemsLoaded$ && !this._menuItemsLoaded$.closed) {
      this._menuItemsLoaded$.complete();
    }

    if (this._menuParentIdLoaded$ && !this._menuParentIdLoaded$.closed) {
      this._menuParentIdLoaded$.complete();
    }
  }

    //ngOnChanges() {
    //  if (this.menuItems == null) {
    //    this.parentMenu = null;
    //  }
    //  this.parentMenu = this.menuItems.filter(item => item.parentId == this.menuParentId);
    //  console.log(this.parentMenu);
    //}


    ngAfterViewInit() {
      this.router.events.subscribe(event => {
        if (event instanceof NavigationEnd) {
          if (this.settings.fixedHeader) {
            let mainContent = document.getElementById('main-content');
            if (mainContent) {
              mainContent.scrollTop = 0;
            }
          }
          else {
            document.getElementsByClassName('mat-drawer-content')[0].scrollTop = 0;
          }
        }
      });
    }

    onClick(menuId) {
      /*************************************
       * Import take operator from 'rxjs/operators'
       * As part of the changes, the values that we need are
       * all on `_menuItemsLoaded$` ReplaySubject. 
       *************************************/
      combineLatest(this._menuItemsLoaded$).pipe(take(1))
        .subscribe(([items]) => {
          this.menuService.toggleMenuItem(menuId);
          this.menuService.closeOtherSubMenus(items, menuId);
          this.onClickMenuItem.emit(menuId);
        }
    }


}

vertical-menu.component.html



<div *ngFor="let menu of parentMenu" class="menu-item">
    <a *ngIf="menu.routerLink && menu.hasSubMenu" mat-button 
        fxLayout="row" [fxLayoutAlign]="(settings.menuType=='default') ? 'start center' : 'center center'"
        [routerLink]="[menu.routerLink]" routerLinkActive="active-link" [routerLinkActiveOptions]="{exact:true}"
        [matTooltip]="menu.title" matTooltipPosition="after" [matTooltipDisabled]="(settings.menuType=='mini') ? 'false' : 'true'"
        (click)="onClick(menu.id)" [id]="'menu-item-'+menu.id">
        <mat-icon class="menu-icon">{{menu.icon}}</mat-icon>
        <span class="menu-title">{{menu.title}}</span>  <!--  !menu.hasSubMenu--> 
    </a>
    <a *ngIf="menu.href && !menu.subMenu && !menu.hasSubMenu" mat-button 
        fxLayout="row" [fxLayoutAlign]="(settings.menuType=='default') ? 'start center' : 'center center'"
        [attr.href]="menu.href || ''" [attr.target]="menu.target || ''"
        [matTooltip]="menu.title" matTooltipPosition="after" [matTooltipDisabled]="(settings.menuType=='mini') ? 'false' : 'true'"
        (click)="onClick(menu.id)" [id]="'menu-item-'+menu.id">
        <mat-icon class="menu-icon">{{menu.icon}}</mat-icon>
        <span class="menu-title">{{menu.title}}</span>        
    </a>
    <a *ngIf="menu.hasSubMenu" mat-button 
        fxLayout="row" [fxLayoutAlign]="(settings.menuType=='default') ? 'start center' : 'center center'"
        [matTooltip]="menu.title" matTooltipPosition="after" [matTooltipDisabled]="(settings.menuType=='mini') ? 'false' : 'true'"
        (click)="onClick(menu.id)" [id]="'menu-item-'+menu.id">
        <mat-icon class="menu-icon">{{menu.icon}}</mat-icon>
        <span class="menu-title">{{menu.title}}</span>
        <mat-icon class="menu-expand-icon transition-2">arrow_drop_down</mat-icon>
    </a>

    <div *ngIf="menu.hasSubMenu" class="sub-menu" [id]="'sub-menu-'+menu.id"> 
        <app-vertical-menu [menuItems]="_menuItemsLoaded$ | async" [menuParentId]="menu.id" (onClickMenuItem)="updatePS($event)"></app-vertical-menu>

     <!-- <app-vertical-menu (onClickMenuItem)="updatePS($event)"
                     [menuItems]="{menuItems: _menuItemsLoaded$ | async, parentId: 0}">
      </app-vertical-menu> -->
    </div>
</div>


sidenav.component.html



<div fxLayout="column" fxLayoutAlign="center center" class="user-block transition-2" [class.show]="settings.sidenavUserBlock"> 
    <div [fxLayout]="(settings.menuType != 'default') ? 'column' : 'row'" 
         [fxLayoutAlign]="(settings.menuType != 'default') ? 'center center' : 'space-around center'" class="user-info-wrapper">
        <img [src]="userImage" alt="user-image">
        <div class="user-info">
            <p class="name">Emilio Verdines</p>
            <p *ngIf="settings.menuType == 'default'" class="position">Web developer <br> <small class="muted-text">Member since May. 2016</small></p>
        </div>
    </div>
    <div *ngIf="settings.menuType != 'mini'" fxLayout="row" fxLayoutAlign="space-around center" class="w-100 muted-text">
        <button mat-icon-button><mat-icon>person_outline</mat-icon></button>
        <a mat-icon-button routerLink="/mailbox">
            <mat-icon>mail_outline</mat-icon>
        </a>
        <a mat-icon-button routerLink="/login">
            <mat-icon>power_settings_new</mat-icon>
        </a>
    </div>
</div>

<perfect-scrollbar #sidenavPS class="sidenav-menu-outer" [class.user-block-show]="settings.sidenavUserBlock">   
    <span *ngIf="!menuItems">loading....</span>
    <app-vertical-menu [menuItems]="menuItems" [menuParentId]="0" (onClickMenuItem)="updatePS($event)"></app-vertical-menu> 
</perfect-scrollbar>

sidenav.component.ts

@Component({
  selector: 'app-sidenav',
  templateUrl: './sidenav.component.html',
  styleUrls: ['./sidenav.component.scss'],
  encapsulation: ViewEncapsulation.None,
  providers: [MenuService]
})
export class SidenavComponent implements OnInit {
  @ViewChild('sidenavPS') sidenavPS: PerfectScrollbarComponent;
  public userImage = '../assets/img/users/user.jpg';
  public menuItems: Array<any>;
  public settings: Settings;
  constructor(public appSettings: AppSettings, public menuService: MenuService) {
    this.settings = this.appSettings.settings;
  }

  ngOnInit() {
    debugger;
    this.menuItems = this.menuService.getVerticalMenuItems();
  }

  ngOnChange() {
    debugger;
    this.menuItems = this.menuService.getVerticalMenuItems();
  }

  public closeSubMenus() {
    const menu = document.querySelector('.sidenav-menu-outer');
    if (menu) {
      for (let i = 0; i < menu.children[0].children.length; i++) {
        const child = menu.children[0].children[i];
        if (child) {
          if (child.children[0].classList.contains('expanded')) {
            child.children[0].classList.remove('expanded');
            child.children[1].classList.remove('show');
          }
        }
      }
    }
  }

  public updatePS(e) {
    this.sidenavPS.directiveRef.update();
  }

dynamic-menu.component.ts


import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators} from '@angular/forms';
import { MatSnackBar } from '@angular/material';
import 'rxjs/add/operator/debounceTime';
import { Menu } from '../../theme/components/menu/menu.model';
import { MenuService } from '../../theme/components/menu/menu.service';
import { DynamicMenuService } from './dynamic-menu.service';
import { AppSettings } from '../../app.settings';
import { Settings } from '../../app.settings.model';
import { VerticalMenuComponent } from '../../theme/components/menu/vertical-menu/vertical-menu.component';
import { listTransition } from '../../theme/utils/app-animation';
import { combineLatest, Observable } from 'rxjs';

@Component({
  selector: 'app-dynamic-menu',
  templateUrl: './dynamic-menu.component.html',
  providers: [ DynamicMenuService, MenuService ],
  animations: [ listTransition ],
  host: {
    '[@listTransition]': ''
  }
})
export class DynamicMenuComponent implements OnInit {
  settings: Settings;
  menuItems:Array<Menu>;
  _menuItems$: Observable<Menu[]>;
  public icons = ['home','person', 'card_travel', 'delete', 'event', 'favorite', 'help' ]
  public form:FormGroup;
  constructor(public appSettings:AppSettings, 
              public formBuilder: FormBuilder, 
              public snackBar: MatSnackBar,
              private menuService:MenuService,
              private dynamicMenuService:DynamicMenuService) {
    this.settings = this.appSettings.settings; 
    this._menuItems$ = this.menuService.getVerticalMenuItems()
        .pipe(tap((menuItems: Menu) => this.menuItems = menuItems));
  }

  ngOnInit() {
    this.form = this.formBuilder.group({
      'title': ['', Validators.compose([Validators.required, Validators.minLength(3)])],
      'icon': null,
      'routerLink': ['', Validators.required],    
      'href': ['', Validators.required],            
      'target': null,
      'hasSubMenu': false,
      'parentId': 0
    });
  }

  ngAfterViewInit() {
    this.form.valueChanges.debounceTime(500).subscribe(menu => {  
      if(menu.routerLink && menu.routerLink != ''){
        this.form.controls['href'].setValue(null);
        this.form.controls['href'].disable();
        this.form.controls['href'].clearValidators();
        this.form.controls['target'].setValue(null);
        this.form.controls['target'].disable();
      }
      else{
        this.form.controls['href'].enable();
        this.form.controls['href'].setValidators([Validators.required]);
        this.form.controls['target'].enable();
      }
      this.form.controls['href'].updateValueAndValidity();

      if(menu.href && menu.href != ''){
        this.form.controls['routerLink'].setValue(null);
        this.form.controls['routerLink'].disable();
        this.form.controls['routerLink'].clearValidators();
        this.form.controls['hasSubMenu'].setValue(false);
        this.form.controls['hasSubMenu'].disable();
      }
      else{
        this.form.controls['routerLink'].enable();
        this.form.controls['routerLink'].setValidators([Validators.required]);
        this.form.controls['hasSubMenu'].enable();
      }
      this.form.controls['routerLink'].updateValueAndValidity();
    })
  }

  onSubmit(menu:Menu):void {
    if (this.form.valid) {
      this.dynamicMenuService.addNewMenuItem(VerticalMenuComponent, this.menuItems, menu);
      this.snackBar.open('New menu item added successfully!', null, {
        duration: 2000,
      });
      this.form.reset({
        hasSubMenu:false,
        parentId:0
      });    
    } 
  } 

}

dynamic-menu.service.ts

import { Injectable, Injector, ComponentFactoryResolver, ApplicationRef, EmbeddedViewRef } from '@angular/core';
import { VerticalMenuComponent } from '../../theme/components/menu/vertical-menu/vertical-menu.component';
import { Menu } from '../../theme/components/menu/menu.model';

@Injectable()
export class DynamicMenuService {

    constructor(private componentFactoryResolver: ComponentFactoryResolver,
                private applicationRef: ApplicationRef,
                private injector: Injector) { }     

    addNewMenuItem(component: any, menuItems:Array<Menu>, menuItem) {

        const lastId = menuItems[menuItems.length-1].id;
        const newMenuItem = new Menu(lastId+1, menuItem['title'], menuItem['routerLink'], menuItem['href'], menuItem['icon'], menuItem['target'], menuItem['hasSubMenu'], parseInt(menuItem['parentId']));

        menuItems.push(newMenuItem);
        let item = menuItems.filter(item=>item.id == newMenuItem.parentId)[0];
        if(item) item.hasSubMenu = true;  

        const componentRef = this.componentFactoryResolver
            .resolveComponentFactory(component)
            .create(this.injector);        

        this.applicationRef.attachView(componentRef.hostView);

        let instance = <VerticalMenuComponent>componentRef.instance;
        instance.menuItems = menuItems;
        instance.menuParentId = 0;

        const elem = (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;

        const sidenav = document.getElementById('sidenav-menu-outer');        
        sidenav.replaceChild(elem, sidenav.children[0]);     

    } 
}

Проверьте изображение для получения дополнительной информации, которая показывает массив нулевой из-за suзадержка подписки введите описание изображения здесь ошибка дублирования, показанная в ссылке ниже введите описание изображения здесь новая ошибка с привязкой крана введите описание изображения здесь

Результирующий вывод введите описание изображения здесь

Ответы [ 2 ]

2 голосов
/ 12 апреля 2019

Ну, я бы изменил несколько вещей в коде.

Использование <vertical-menu>

Вместо:

<app-vertical-menu [menuItems]="menuItems" [menuParentId]="0" (onClickMenuItem)="updatePS($event)"></app-vertical-menu> 

Я бы сделал:

<app-vertical-menu [menuItems]="_menuItems$ | async" [menuParentId]="0" (onClickMenuItem)="updatePS($event)"></app-vertical-menu> 

Изменение menuItems на _menuItems$ является просто косметическим, чтобы указать, что с этого момента оно станет наблюдаемым. Настоящее изменение - труба async.

В машинописном коде, который вызывает menuService

_menuItems$: Observable<Menu>;

ngOnInit() {
  this._menuItems$ = this.menuService.getVerticalMenuItems();
}

MenuService

import {filter, map} from 'rxjs/operators';

...

getVerticalMenuItems(): Observable<Menu[]> {  

  return this.workoutService.getMenus().pipe(
    // this will avoid null/undefined values
    filter(Boolean),
    // this will avoid empty arrays comming from getMenus()
    // just comment it if empty arrays are allowed here
    filter(data => !!data.length),
    // here we're building the Menu array
    map((data:any) => data.map(i => new Menu(i.id, i.title, 
                                             i.routerLink, i.href,
                                             i.icon, i.target, 
                                             i.hasSubMenu, i.parentId))
  ); // here we're closing the pipe

}

Делая подобные вещи, мы передадим async каналу ответственность за подписку и отказ от подписки на сервис меню.

Я не уверен, что getMenu() делает здесь. Будет ли захватывать данные меню с сервера? Если так, разве не более эффективно кешировать данные меню в сервисе, когда приложение запускается вместо того, чтобы каждый раз заходить на сервер? Просто предположение, потому что я не совсем понимаю, как вы создали вещи в своем приложении.

UPDATE

Поскольку я пропустил еще одну ошибку, я добавляю еще некоторые изменения в другие компоненты.

VerticalMenuComponent

В этом компоненте у вас есть:

@Input('menuItems') menuItems;
@Input('menuParentId') menuParentId;

...

ngOnInit() {
  this.parentMenu = this.menuItems.filter(item => item.parentId == this.menuParentId); -- not working??
}

Для успешной инициализации parentMenu вы должны быть уверены, что загружены menuItems и menuParentId. Это небезопасно делать, как будто это делается внутри OnInit. Я собираюсь предложить очень надежный IMO (я использую его в некоторых моих компонентах) и использовать мощный API RxJ.

ОБНОВЛЕНО 2 (filter(Boolean) нельзя использовать для menuParentId):

// We must save the received items and also emit it
@Input() 
set menuItems(items: Menu[]) {
  this._menuItemsLoaded$.next(items);
}
private _menuItemsLoaded$: ReplaySubject<Menu[]> = new ReplaySubject<Menu[]>(1);

// We must save the received items and also emit it
@Input() 
set menuParentId(items: number) {
  this._menuParentIdLoaded$.next(items);
}
private _menuParentIdLoaded$: ReplaySubject<number> = 
  new ReplaySubject<number>(1);

...

ngOnInit() {
  combineLatest(
    this._menuItemsLoaded$.pipe(
          filter(Boolean), 
          filter((_) => !!_.length), 
          debounceTime(300)),
    this._menuParentIdLoaded$.pipe(
          // for _menuParentId (exclusively) we cannot use 
          // filter(Boolean) here, or the 0 values will be blocked
          filter((_) => _ !== null && _ !== undefined),
          debounceTime(300)),
  ).subscribe(([menuItems, parentId]) => 
    this.parentMenu = menuItems.filter(item => item.parentId == parentId)
  );
}

// as we're using some subjects, we must finalize
// them in case this component is eventually destroyed
ngOnDestroy() {
  if(this._menuItemsLoaded$ && !this._menuItemsLoaded$.closed) {
    this._menuItemsLoaded$.complete();
  }

  if(this._menuParentIdLoaded$ && !this._menuParentIdLoaded$.closed) {
    this._menuParentIdLoaded$.complete();
  }
}

Другой подход

Если вы думаете, что приведенный выше подход является своего рода подавляющим (что я согласен, но я думаю, что это также очень полезно, чтобы привыкнуть к RxJs), и вы имеете полный контроль над API вашего VerticalMenuComponent и его @Input() можно создать интерфейс данных компонента меню и передать только один параметр:

export interface VerticalMenuComponentData {
  menuItems: Menu[];
  parentId: number;
}

...

// We must save the received items and also emit it
@Input() 
get verticalMenuComponentData: VerticalMenuComponentData { return this._verticalMenuComponentData; }
set verticalMenuComponentData(data: VerticalMenuComponentData) {
  this._verticalMenuComponentData = data;

  // You don't need to call anything inside ngOnInit
  this.parentMenu = data && data.menuItems && data.parentId 
    ? data.menuItems.filter(item => item.parentId == data.parentId) 
    : [];
}
private _verticalMenuComponentData: VerticalMenuComponentData;

И тогда вам следует немного изменить шаблон:

<app-vertical-menu (onClickMenuItem)="updatePS($event)"
                   [menuItems]="{menuItems: _menuItems$ | async, parentId: 0}">
</app-vertical-menu> 
1 голос
/ 11 апреля 2019

При вызове подписки () ваш код не перестает ждать ответа сервера.Вместо этого он продолжает выполнять оставшуюся часть getVerticalMenuItems().Как только http-запрос завершен, выполняется функция, указанная вами в качестве параметра subscribe().Но это может быть секундой или двумя позже.Таким образом, ваш console.log(--END OF SUBSCRIBE---) на самом деле не соответствует действительности, это не конец subscribe(), subscribe() еще не завершен на данном этапе выполнения.

Что вы можете сделать сейчас: сделать resultMenu переменной компонента и сохранить данные там.Затем в html вы можете использовать эту переменную в обычном режиме, например:

<ul *ngFor="let menu of resultMenu">
  <li>
    //other code
    {{menu.title}}
  </li>
</ul>

Angular обнаружит, что ваш this.resultMenu изменился, и обновит представление автоматически (если для параметра обнаружения обнаружения не установлено значение onPush, тогда выдолжен сделать это).Вы увидите, что ваше меню появится, как только сервер ответит.

Если вам нужно что-то сделать с полученными данными перед их отображением, вы должны реализовать ngOnChanges и сделать это там.Это называется всякий раз, когда изменяется вход компонента.

Вот демонстрация Stackblitz , посмотрите журналы консоли

...