Jose Rojas
Jose Rojas

Reputation: 3520

call method on component through @ViewChild in views into a QueryList

I want to do a component reusable, setting multiple children and render one component at time, I have a collection of views got with @ContenChildren, at parent component, something like this:

@ContentChildren(ChildComponent) children: QueryList<ChildComponent>;

//getCurrentChild function
getCurrentChild(){
   //index is a position of child
   return this.children.toArray()[this.index];
}  

and render parent template:

<ng-container *ngTemplateOutlet="getCurrentChild().content"></ng-container>

content is rendered in the child component with @ViewChild:

@ViewChild('currentChild') child;

and the template looks like this:

<ng-template #currentChild>
  <ng-content>
  </ng-content>
</ng-template>

When I want to implement this structure, I do something like this:

<parent>
    <child>
       <specific-one></specific-one>
    </child>
    <child>
       <specific-two></specific-two>
    </child>
</parent>

Now I have a method in the parent component that is fired on click button and need to call a method in the specific components (specific-one or specific-two depending on currentChild):

export class SpecificOneComponent implements OnInit{

      action(){
          //this action will be called when parent is clicked
      }

}

I've tried calling method trough child reference but the action doesn't exit. also passing a context and neither. It looks like the way I'm setting the content in parent template is not right.

Any help would be appreciate it.

Upvotes: 1

Views: 1013

Answers (1)

Andrei Gătej
Andrei Gătej

Reputation: 11934

Every child component will have its own content, so this means that it would be enough to only get the reference to the ng-template in which there would be ng-content.

Also, because you want to call methods from specific components when a certain event occurs on the parent, we are going to use a service in order to be able to notify the specific components.

specific.service.ts

  private _shouldCallAction$ = new Subject();
  shouldCallAction$ = this._shouldCallAction$.asObservable();

  constructor() { }

  triggerAction (uniqueCompId) {
    this._shouldCallAction$.next(uniqueCompId);
  }

specific-{one|two|...n}.component.ts

This goes for every component that depends on some events that occur in the parent component.

  private shouldCallActionSubscription: Subscription;

  uniqueId: number | string;

  constructor(private specificService: SpecificService) {
    this.uniqueId = randomId();
  }

  ngOnInit() {
    this.shouldCallActionSubscription = this.specificService.shouldCallAction$
      .pipe(
        filter(id => id === this.uniqueId)
      )
      .subscribe(() => {
        console.log('calling action for specific-one')
      });
  }

  ngOnDestroy () {
    this.shouldCallActionSubscription.unsubscribe();
  }

child.component.html

<ng-container *ngIf="instanceIdx === crtIdx">
  <h3>trigger action of current specific component</h3>
  <button (click)="triggerAction()">trigger</button>
</ng-container>

<ng-template #currentChild>
  <ng-content></ng-content>
</ng-template>

child.component.ts

Here you'll also need to get a reference to the specificComponent in order to get its unique id.

// Before class
let instances = 0;

@ViewChild('currentChild', { static: true }) tpl: TemplateRef<any>;
@ContentChild('specific', { static: true }) specificComp;

  get uniqueSpecificCompId () {
    return this.specificComp.uniqueId;
  }

  constructor (private specificService: SpecificService) {
    this.instanceIdx = instances++;
  }

  triggerAction () {

    this.specificService.triggerAction(this.uniqueSpecificCompId);
  }

parent.component.ts

  @ViewChildren(ChildOneComponent) children: QueryList<ChildOneComponent>;
  @ViewChild('container', { static: true, read: ViewContainerRef }) container: ViewContainerRef;

  crtIdx = 0;

  ngAfterViewInit () {
    this.setNewView();
  }

  setNewView () {
    this.container.clear();

    this.container.createEmbeddedView(this.children.toArray()[this.crtIdx].tpl);
  }

  updateIndex (idx) {
    this.crtIdx = idx;

    this.setNewView();
  }

parent.component.html

<app-child [crtIdx]="crtIdx">
 <!-- ... -->
 <app-specific-two #specific></app-specific-two>
</app-child> 
<app-child [crtIdx]="crtIdx">
 <!-- ... -->
 <app-specific-one #specific></app-specific-one>
</app-child> 

<ng-container #container></ng-container>

<h3>Select a child</h3>

<button
  *ngFor="let _ of [].constructor(n); let idx = index;"
  (click)="updateIndex(idx)"
> 
  Select {{ idx + 1 }}
</button>

Here is the demo.

Best of luck!

Upvotes: 2

Related Questions