QueryList not updated with dynamic component

Viewed 9798

I have a parent component which can have multiple instances of the same child component. The tricky part here is that an instance of the child component is available at init and based on certain logic within child component I am using a output event emitter to regenerate another child component from the parent component. However, when saving I just see the first instance of the child component which I had put in my html template and not the dynamically generated one. I did see other similar questions but none seem to had the same problem. What am I missing here?

Any help would be greatly appreciated.

UPDATE - start

Added a plunker to demonstrate the issue if someone wants to play around and find the cause

https://plnkr.co/edit/FW49ztzsg5uyXF7DtHDY?p=preview

UPDATE - end

Parent Component

import { Component, OnInit, ViewContainerRef, ViewChild, ComponentFactoryResolver, ViewChildren, QueryList } from '@angular/core';
import { ChildComponent } from './child/child.component';

@Component( {
    selector: 'parent-cmp',
    entryComponents: [ChildComponent],
    template: `
        <child-cmp (onSomeSelectionChange)="onSomeSelectionChange($event)"></child-cmp>
        <div #childCompSection></div>
        <button (click)="save()">Save</button>
    `
} )
export class ParentComponent implements OnInit {

    @ViewChild( 'childCompSection', { read: ViewContainerRef } ) childCompSection: any;
    currentComponentHolder: any;
    @ViewChildren( ChildComponent ) childComponents: QueryList<ChildComponent>;

    constructor( private resolver: ComponentFactoryResolver ) { }

    ngOnInit() {
        // do some stuff to load initial view, nothing done using childComponents
    }

    onSomeSelectionChange( someValue ) {
        if ( someValue !== 3 ) {
            let componentFactory = this.resolver.resolveComponentFactory( ChildComponent );
            this.currentComponentHolder = this.childCompSection.createComponent( componentFactory );
            this.currentComponentHolder.instance.onSomeSelectionChange.subscribe( data => this.onSomeSelectionChange( data ) );
        }
    }

    // Need all child components (which includes dynamically generated components) here 
    save() {
        console.log(this.childComponents); // This just prints the child-cmp object that is already added not the ones that are dynamically added via ComponentFactoryResolver
    }

}

Child Component

import { Component, Output, EventEmitter } from '@angular/core';
import { ChildComponent } from './child/child.component';

@Component( {
    selector: 'child-cmp',
    template: `
        <div>
            <select [(ngModel)]="selectedValue" (ngModelChange)="showNumberField($event)">
                <option value="0">Select...</option>
                <option value="1">Add...</option>
                <option value="2">Subtract...</option>
                <option selected value="3">End</option>
            </select>
            <div *ngIf="showField">
                <label>Number:</label><input type="number" [(ngModel)]="numValue">
            </div>
        </div>
    `
} )
export class ChildComponent {

    @Output() onSomeSelectionChange = new EventEmitter<Lookup>();
    selectedValue: number;
    numValue: number;
    showField: boolean = false;

    showNumberField(value) {
        if(value !== 3) {
            showField = true;
        }
        this.onSomeSelectionChange.emit( value ); // This does work fine and another instance of the child component is generated from parent component but QueryList in parent component does not get updated
    }
}
6 Answers

I think this Angular bug is still not fixed. I had to manually remove the element, something like below, similarly it can be updated manually.

@ViewChildren('configElement') public configValues: QueryList<ConfigValueComponent>; (Declaration)
this.configValues['_results'].splice(data.index, 1); (remove deleted element)

@ViewChildren and @ContentChildren will update for dynamic content, but getting there is tricky.

The issue goes like this:

  • In a @Component, the model T[] updates
  • A *ngFor binding updates
  • QueryList remains unchanged even though *ngFor changed

When you think about it, it makes sense. The CD-run that updated that *ngFor has already passed. To get around it, you will need to make Angular run CD again. Let's add some steps:

  • In @Component, the model T[] updates
  • In @Component we run this.changeDetectorRef.markForCheck(); to flag the view as dirty
  • In @Component we run this.zone.run(() => Promise.resolve()); to tick the app
  • A *ngFor binding updates
  • QueryList remains unchanged even though *ngFor changed
  • The Promise fires and a second CD-run starts
  • The QueryList updates since the view was flagged as dirty

Use @ViewChildren like you did:

@ViewChildren("elements")
private elementsArray!: QueryList<any>;

When you need the update:

this.elementArray.changes
        .subscribe((queryChanges) => {
            ...
        });

Finally, to avoid errors and detect changes, in the constructor:

private ref : ChangeDetectorRef

And then below ngOnInit(),

ngAfterContentChecked() {
    this.ref.detectChanges();
}

Works for me, gl :)

Related