Identify specific Angular TemplateRef from a QueryList

Viewed 5248

In Angular 6/7, I have a component into which I am projecting content like so (ParentComponent template):

<my-component [templateNames]="['t1', 't2']">
  <ng-template name="t1">...</ng-template>
  <ng-template name="t2">...</ng-template>
  <ng-template>...</ng-template> <!-- not included in [templateNames] -->
</my-component>

In the MyComponent class, I can get a QueryList of all the templates using the ContentChildren decorator:

@ContentChildren(TemplateRef) templates: QueryList<TemplateRef<any>>;

The challenge is that I want to execute code on specific templates identified by what ParentComponent set via the @Input() templateNames.

processTemplates() {
  for (const name of this.templateNames) {
    const templateRef = this.getTemplateByName(name);
    this.doWork(templateRef);
  }
}

getTemplateByName(name) {
  const templates = this.templates.toArray();

  return templates.find(t => ?); // what can I query to distinguish templates?
}

Problem is that I don't know how to read the name attribute or anything else I set on the ng-template tag in ParentComponent. I have no idea how to distinguish one TemplateRef from another;

Keep in mind that MyComponent cannot make any assumption on what names will be used, or whether all ng-templates should be processed -- the last one in my example above should not get processed because it's not listed in the @Input() templateNames. Is there anything I can set in ParentComponent that will help me tell the two TemplateRef's apart?

2 Answers

You can create a directive with a name input parameter:

@Directive({
  selector: '[template-name]'
})
export class TableColumnDirective {

  constructor(public readonly template: TemplateRef<any>) { }

  @Input('template-name') columnName: string;
}

Use that way:

  <my-component>
      <ng-template template-name="t1">...</ng-template>
      <ng-template template-name="t2">...</ng-template>
      ...

And then in my-component inject that way:

@ContentChildren(TableColumnDirective) templates: QueryList<TableColumnDirective>;

For a more detailed explanation/example look at the accepted answer from this question

You can either choose on one of these methods:

If its only for 2 components, you can access them using QueryList getters (first and last)

@ContentChildren(TemplateRef) templates: QueryList<TemplateRef<any>>;

ngAfterContentInit() {
    console.log(this.templates.first);    // Gives you the 1st template child
    console.log(this.templates.last);     // Last template child (2nd child)     
}

Find by Index

this.templates.find((template, index) => index == 1); // 2nd template child

Other alternative

Had created a Stackblitz Demo using an extension on Components

1.) Create TemplateContentComponent This will serve as your ChildComponent and add @Input()

    @Component({
      selector: 'template-content',
      template: `
          // If no ng-template reference available, show its ng-content
          <ng-content *ngIf="!template"></ng-content>

         // Else, show the ng-template through ng-container
         <ng-container *ngIf="template"
                       [ngTemplateOutlet]="template"></ng-container>
      ` 
    })
    export class TemplateContentComponent {
        @Input() name: string;    // Serves as your component id
    }

2.) Create TemplateContainerComponent - This will serve as your ParentComponent

 @Component({
  selector: 'template-container',
  template: `<ng-content></ng-content>`
})
export class TemplateContainerComponent implements AfterContentInit  {

    @ContentChildren(TemplateContentComponent) templates: QueryList<TemplateRef<any>>;

      ngAfterContentInit() {
        // You can now check whether you'll be fetching a template
        // based on the names you want provided from parent template.

        const t1 = this.templates.find((template: any) => template.name === 't1');

        console.log(t1);   // This will show the t1 component
                           // which t1 and t2 are the same component
                           // but had set a name @Input() as their ID
      }

    }

Result

3.) On your AppComponent Template

<template-container>
  // Can be a raw template, good for ng-content
  <template-content [name]="'t1'">t1 template</template-content>

  // Or a template from ng-template, good for ng-container
  <template-content [name]="'t2'"
                    [template]="userList"></template-content>
</template-container>


// User List Template
<ng-template #userList>
  <h1>User List</h1>
</ng-template>

Template

Related