8

Say I have this template:

<div class="main__container">
    <ng-template #test></ng-template>
    <ng-template #test2></ng-template>
</div>

And I retrieve a reference to all TemplateRef via:

@ViewChildren(TemplateRef)
private templates;

How can I, for all of them, retrieve the name of the associated variable? So "test" for the first template and "test2" for the other one. I don't absolutely want to use variables, if it's possible by using an attribute of ng-template, it's OK. I tried with TemplateRef, ViewContainerRef and ElementRef but everytime, it returns a reference to a comment element.

ssougnez
  • 5,315
  • 11
  • 46
  • 79

4 Answers4

7

I also had to be able to set templateRef dynamically. The goal is to choose a template based on a dynamic type.

My solution (https://stackblitz.com/edit/angular-dynamic-templateref):


@Directive({
  selector: "ng-template.[typeTemplate]"
})
export class TypeTemplateDirective {
  @Input()
  typeTemplate: string;

  constructor(public templateRef: TemplateRef<any>) {}
}

@Component({
  selector: "type-template",
  template: `
    <ng-container *ngTemplateOutlet="(template$ | async)"></ng-container>

    <ng-template typeTemplate="typeA">Template for type A</ng-template>
    <ng-template typeTemplate="typeB">Template for type B</ng-template>
  `
})
export class TypeTemplateComponent implements AfterViewInit {
  @ViewChildren(TypeTemplateDirective)
  private typeTemplateDirectives: QueryList<TypeTemplateDirective>;

  @Input()
  set type(newType: string) {
    this.type$.next(newType);
  }

  type$ = new BehaviorSubject<string>(null);
  template$ = this.type$.pipe(
    filter(() => !!this.typeTemplateDirectives),
    map(
      type =>
        this.typeTemplateDirectives.find(
          directive => directive.typeTemplate === type
        ).templateRef
    )
  );

  ngAfterViewInit(): void {
    this.type$.next(this.type$.getValue());
  }
}

So when a new type is pushed to , the correct template will be used.

Michael Wyraz
  • 3,638
  • 1
  • 27
  • 25
JoG
  • 962
  • 2
  • 11
  • 17
  • 3
    This should be the accepted answer because it's the one that solves this without exploiting internal apis. With PrimeNG there's pTemplate that does exactly this. – Michael Wyraz Jul 14 '21 at 09:15
6

I managed to do that using TemplateRef's private API. Here's the function I use at the moment :

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

getTemplate(id: string): TemplateRef<any> {
    // FIXME: Should not use private API...
    return this.templates ? this.templates.find((template: any) => 
    template._def.references[id]) : null;
}

But this doesn't work with a production build, actually I found your post looking for another solution, I'll update if I find something or submit a feature request to Angular github. In the meantime I thought it would be worth sharing.

-- EDIT -- It really looks like there is a possible solution using a structural directive.

You'll learn in this guide that the asterisk (*) is a convenience notation and the string is a microsyntax rather than the usual template expression. Angular desugars this notation into a marked-up <ng-template> that surrounds the host element and its descendents. Each structural directive does something different with that template.

Link to documentation

-- EDIT 2 -- Ok it works with a directive that retrieve the TemplateRef, but for the moment I register it to a service using an id, I don't use ViewChildren/ContentChildren anymore. My Component which use *ngTemplateOutlet directive then retrieve the TemplateRef using the service.

Here's the directive source code :

@Directive({
     selector: '[formeTemplate]'
})
export class FormeTemplateDirective {
    @Input('formeTemplate') templateId;

    constructor(private host: TemplateRef<any>, private service: FormeTemplateService) { }

    ngOnInit() {
        this.service.register(this.templateId, this.host);
    }
}

The service :

@Injectable()
export class FormeTemplateService {
    private templates: Array<FormeTemplate>;

    constructor() {
        this.templates = new Array();
    }

    register(id: string, ref: TemplateRef<any>) {
        this.templates.push(new FormeTemplate(id, ref));
    }

    getTemplateRef(templateId: string) : TemplateRef<any> {
        let template = this.templates.find((template) => template.id === templateId);
        return template ? template.ref : null;
    }
}
Elvynia
  • 337
  • 2
  • 11
0

You can access the template variable by asking for them by name as follows:

 @ViewChildren('test, test2') templates;

Here is a simple Plunker demonstrating the behavior.

Teddy Sterne
  • 13,774
  • 2
  • 46
  • 51
  • 1
    Yes I know this but I'd like my component to be more generic. I want to be able to put as many `ng-template` as I want and retrieve them in code. However, I also need a way to identify them because I have a json object like `{ test: { ... }, test2: { ... } }` that I want to use to do some stuff. – ssougnez Mar 03 '17 at 14:57
  • You can't dynamically add template variables so there will never be more than those created a design time. Since this is the case if you can just reference them by name and access the data from the object using the predefined name. – Teddy Sterne Mar 03 '17 at 15:08
  • 3
    I don't want to dynamically add template variables. What I want could be achieved in two ways: Either be able to dynamically retrieve variable from my template, so something like `ViewChildren` but during runtime (something like `let tpl: TemplateRef = this.viewChildren.get(name);`), or being able to retrieve all `ng-template` element from the template and have a way to get the name of the variable attached to them. – ssougnez Mar 03 '17 at 15:12
0

I had the same problem. My solution to this is illustrated in this simplyfied example:

Usage:

<app-my-component [templates]="{ highlight: highlightTemplate, italic: italicTemplate }" [useTemplate]="'highlight'">
    <ng-template #highlightTemplate let-contentRef="contentRef">
        <span class="highlighted-text"><ng-container *ngTemplateOutlet="contentRef"></ng-container></span>
    </ng-template>
    <ng-template #italicTemplate let-contentRef="contentRef">
        <span class="font-italic"><ng-container *ngTemplateOutlet="contentRef"></ng-container></span>
    </ng-template>
</app-my-component>

Template of myComponent:

<div class="my-component">
    <ng-template #contentRef>The content which is given to the customTemplate</ng-template>
    <ng-container *ngTemplateOutlet="customTemplate; context: { contentRef: contentRef }" >
    </ng-container>
</div>

Class definition:

export class MyComponent {

    @Input() private templates: any = {};
    @Input() private useTemplate: string;

    get customTemplate() {
        return this.templates[this.useTemplate];
    }

}
Chris
  • 111
  • 1
  • 8