8

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
    }
}
Sayantan
  • 564
  • 2
  • 7
  • 23

6 Answers6

8

I came across the same issue and found that you can subscribe to the query list changes which will fire if you add or remove an element. Hopefully will help someone else down the line...

@ViewChildren(MyComponent) myComp: QueryList<MyComponent>;

ngAfterViewInit() {
    this.myComp.changes
            .subscribe((queryChanges) => {
                // Do something
            });
}
Joe Keene
  • 2,175
  • 21
  • 27
  • Was this recently added in an version upgrade? Just want to make sure if this was available from the beginning or was just a recent addition. Also, did you try to replicate the issue I have mentioned with the above? I am not too sure this will resolve the issue I stated. – Sayantan Jan 25 '18 at 06:45
5

Got a confirmation from the Angular chat forum that @ViewChildren/@ViewChild can only read elements which are already available in the view. Since, dynamic components are not available during view init, I had to maintain a list of all such dynamically generated components to keep track of them.

Sayantan
  • 564
  • 2
  • 7
  • 23
  • How did you maintain that list? I have a bunch of dynamic component instances, can create as many instances as needed, or delete as many, and still trying to find a way to get a hold of the current list of these instances on click action. `@ViewChildren` does not work for me, always returns empty. – j4v1 Apr 20 '18 at 15:19
  • 1
    You have to dynamically generate components using ComponentFactoryResolver, there are tons of examples if you google. Now when you create using the resolver you will get an object of componentHolder (define the type as any), and you can get the component instance by componentHolder.instance, and then add this to an array of such components which you now can maintain. Hope this helps. – Sayantan May 18 '18 at 06:32
2

After all your child components ngAfterViewInit method will execute

export class ParentComponent implements OnInit, AfterViewInit {

 ngAfterViewInit(){

     ///////////////// your operations
 }

So you cannot perform operations in ngOnInit

Aravind
  • 40,391
  • 16
  • 91
  • 110
  • I am not using childComponents in ngOnInit, it is used in save() which is called on a button click from the parent component which is after view init. And as I said, all my child components are not available after view init as they are dynamically generated. Have modified sample as well to reflect where I am looking for all the child components – Sayantan May 13 '17 at 00:25
  • It is super important to note that the code must go in ngAfterViewInit *and not* ngOnInit – David Poxon Jan 05 '18 at 07:48
  • @DavidPoxon are you asking me something? – Aravind Jan 05 '18 at 09:28
  • @Aravind Just reaffirming your contribution. I scratched my head for a long while as I was including code in ngOnInit, not ngAfterViewInit. – David Poxon Jan 06 '18 at 03:27
2

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 :)

0

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)
Manish Jain
  • 9,569
  • 5
  • 39
  • 44
0

@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
blid
  • 971
  • 13
  • 22