Identify specific Angular TemplateRef from a QueryList
Asked Answered
F

2

4

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?

Fontainebleau answered 20/10, 2018 at 8:35 Comment(0)
B
3

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

Becka answered 20/10, 2018 at 10:25 Comment(6)
Thanks. Those are cool ideas but they won't work for me. The parent component needs to be the one to assign ids to TemplateRefs for my use case. There may be TemplateRefs that should be ignored (only the parent knows which) and the child MyComponent does not know how many TemplateRefs there will be, nor in what order they will be.Fontainebleau
I have reworded the question to make the requirements more clear.Fontainebleau
Had updated my solution, mind if you could check and if it applies to your current problem ? Had added a stackblitz demo link or you can visit it here stackblitz.com/edit/ngx-content-templateref and check the preview's bottom part for the console result from the parent component console log for the querylistBecka
It didn't use an ng-template since it somehow difficult to check a reference on them, so had somehow create a template-content component that would be used as your base template that acts as an ng-template that also accepts an inner template content inside.Becka
Hey your solution is really creative; +1. It's more complicated than the solution I have now though. I am currently using @ViewChild() (one for each templateRef) in the parent component class to get a reference to the TemplateRefs. I can then send the TemplateRefs directly view @Input() to the MyComponent so the input goes from ['t1', 't2'] to [{name: 't1', templateRef: ref1}, {name: 't2', templateRef: ref2}]. That works but I was hoping to find something more streamlined; that's why I posted my problem here.Fontainebleau
That's great. Apologies, my solution was quite complicated but I'm happy you were able to sort it your way. Hoping there will be also solution that will be posted here soon as im also curious for other solutions as well. Thank you.Becka
L
3

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

Li answered 12/2, 2021 at 12:36 Comment(1)
This works great. Two quick tips: 1) Don't forget to add the directive to your module or templates will be empty. 2) templates won't be initialized until ngAfterContentInit()Lesbos

© 2022 - 2024 — McMap. All rights reserved.