Search code examples
angularngforviewchild

Angular ViewChildren does not see all children from ngFor immediately


I have a strange behaviour of @ViewChildren corresponding to children components generated by ngFor. @ViewChildren query does not see element standing in array for a quite long time. All my code is in the Plunker - see with console opened.

This is my main component:

@Component({
    selector: 'my-app',
    template: `
        <button (click)="addInternalComponent()">Add internal component</button>
        <app-internal #internals *ngFor="let i of indexes" [index]="i
(afterViewInit)="onAfterViewInit()"></app-internal>
    `,
})
export class App {
    @ViewChildren('internals') internals: QueryList<InternalComponent>;
    indexes = [];
    addInternalComponent() {
        console.log('adding internal component!');
        this.indexes.push(this.indexes.length);
        console.log('Not complete list', this.internals._results);

    }

    onAfterViewInit() {
        console.log('onAfterViewInit - still not complete list', this.internals._results);
    }
}

Which has come children components that we can add by clicking a button.

After adding an element into indexes array that generates all children in ngFor loop - we don't have that one that we just have added.

I can understand that behaviour - because maybe child component needs some time to create it and engine decides to console log before creating full child component.

However I created emitter in child component signaling that both child's view and model are initialised. But... Handling this event in parent component - we still somehow doesn't have this item. How even is this possible?

Child component:

export class InternalComponent implements AfterViewInit {
  @Input()
  index;
  @Output()
  afterViewInit: EventEmitter<any> = new EventEmitter<any>();

  ngAfterViewInit() {
    this.afterViewInit.emit();
  }

}

Solution

  • The standard way to be notified that the content of a QueryList has changed is to subscribe to its changes event in ngAfterViewInit:

    @ViewChildren("internals") internals: QueryList<InternalComponent>;
    
    ngAfterViewInit() {
      this.internals.changes.subscribe((list: QueryList<InternalComponent>) => {
        // The updated QueryList is available here (with list or with this.internals)
        this.doSomethingWithInternals(list);
        this.doSomethingWithNewInternal(list.last);
        ...
      });
    }
    

    The event handling above may be all you need. If you still want to implement the afterViewInit event in InternalComponent, you can pass a reference to the component as a parameter of the event:

    export class InternalComponent implements AfterViewInit {
      @Output() afterViewInit = new EventEmitter<InternalComponent>();
    
      ngAfterViewInit() {
        this.afterViewInit.emit(this);
      }
    
    }
    

    and retrieve the component in the event handler:

    (afterViewInit)="onAfterViewInit($event)"
    
    onAfterViewInit(component: InternalComponent) {
        this.useNewInternalComponent(component);
        ...
    }