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?

BeetleJuice
  • 39,516
  • 19
  • 105
  • 165

2 Answers2

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

lujop
  • 13,504
  • 9
  • 62
  • 95
  • 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()` – Simon_Weaver Feb 26 '21 at 18:35
1

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

KShewengger
  • 7,853
  • 3
  • 24
  • 36
  • 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. – BeetleJuice Oct 20 '18 at 13:07
  • I have reworded the question to make the requirements more clear. – BeetleJuice Oct 20 '18 at 13:56
  • 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 https://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 querylist – KShewengger Oct 20 '18 at 14:03
  • 1
    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. – KShewengger Oct 20 '18 at 14:14
  • 1
    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 `TemplateRef`s. 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. – BeetleJuice Oct 20 '18 at 16:55
  • 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. – KShewengger Oct 20 '18 at 22:21