1

I checked: ViewChildren for ng-template and Access multiple viewchildren using @viewchild

But I am not able to call my template via the value of a variable...

So my template is like this:

<ng-container *ngFor="let feature of Object.values(features)">
  <ng-container *ngTemplateOutlet="templates[feature]"></ng-container>
</ng-container>

<ng-template #myFeature>
  Nothing to see here
</ng-template>

<ng-template #myOtherFeature>
  Nothing to see here
</ng-template>

features is an enum with matching values to my templates names... then in my class, I tried to grab all the ViewChildren like this:

export class SomeClass {
   @ViewChildren(TemplateRef) templates: QueryList<TemplateRef<any>>;
}

So the idea is that, i thought I should be able to reference the correct template by doing templates[feature] which should yield something like templates['myFeature'] and give me the right template... but is not.

How do I archive this?

codenamezero
  • 2,724
  • 29
  • 64
  • [ngTemplateOutlet]="templates['myFeature']" ? – xFly.Dragon Jun 21 '19 at 03:01
  • If all you need is [ngTemplateOutlet]="templates['myFeature']" then can't you just have all the template names in a string[ ] i.e. ```public templates : string[ ] = ['myFeature', 'myOtherFeature']``` – user2713706 Jun 21 '19 at 04:37
  • Is there a way to avoid defining a the available templates manually?? That's what I tried to avoid with the `templates` variable. – codenamezero Jun 21 '19 at 10:42

3 Answers3

1

Since you have created different templates(different template variables), you need to create different view child for each of them. ViewChildren will work only if they are of same template reference variable. And the usage in your code it will fetch every template instance because you are passing TemplateRef, it will fetch every instance of this type.

I have created a stackblitz, which demonstrates this.

Also note, you template instance will be available only on ngAfterViewInit(), until then it would be undefined.

KiraAG
  • 773
  • 4
  • 19
  • Is there a way to avoid defining a `ViewChild` for every template I got? I find this pretty stupid? I mean I may as well use a switch and call the template myself manually? – codenamezero Jun 21 '19 at 10:41
  • No I don't think so. Maybe someone else might have a different approach. – KiraAG Jun 21 '19 at 10:54
  • @codenamezero See my answer below to get what you are looking for with a single variable – Elias Faraone Apr 15 '20 at 11:05
0

After some tinkering inside the ngAfterViewInit, I got it working to the way I want it. Is a little bit ugly because I need to use a setTimeout and I need to play with the internal variables (not sure if this is a good idea)...

Here is a stackblitz the showcase dynamic template selection and rendering by variable value.

In a nutshell, here is how I did it, you need 3 things:

// to grab all the templates
@ViewChildren(TemplateRef) templates: QueryList<TemplateRef<any>>;
// to be use for template selection
templateMap: { [key: string]: TemplateRef<any> } = {};
// to avoid rendering during the first run when templateMap is not yet ready
templateMapReady = false;

Then in the ngAfterViewInit you do the following to build the templateMap:

ngAfterViewInit(): void {
  // setTimeout to bypass the ExpressionChangedAfterItHasBeenCheckedError
  setTimeout(() => {
    // loop through the fetched template
    this.templates.toArray().forEach(t => {
      // for those ng-template that has a #name, they will have references
      const keys = Object.keys((t as any)._def.references);
      if (keys.length === 1) {
        // so we put these in the templateMap
        this.templateMap[keys[0]] = t;
      }
    });
    // now we change it to ready, so it would render it
    this.templateMapReady = true;
  });
}
codenamezero
  • 2,724
  • 29
  • 64
0

EXPLANATION

The ViewChildren directive will have its value stored right before ngAfterViewInit() (see this).

Angular checks your template at first and finds templates to be undefined.

It then starts to render the view. In the process, it resolves Template directives like ViewChildren() and calls ngAfterViewInit().

In the process, templates gets set which means the view is now in an inconsistent state.

The initial rendering of the page is causing a change to the page itself.

That's when you get the infamous "Expression has changed ..." error.

SOLUTION

You can't change when templates is set, as it's orchestrated by Angular.
What you can do though, is instead use another variable for the binding, and set it to templates once the initial view rendering is done.

Trying to set our new variable in ngAfterViewInit() will again trigger the "Expression has changed" error, since this life cycle hook itself is part of the initial rendering.

The solution is to defer the setting of the new variable in ngAfterViewInit() to the next VM turn.

To do this, we can simply use setTimeout() with no second argument:

export class AppComponent implements AfterViewInit {
  @ViewChildren(TemplateRef) templates!: QueryList<TemplateRef<any>>;
  features: TemplateRef<any>[] = [];
  name = "Angular";

  ngAfterViewInit() {
    setTimeout(() => this.features = this.templates.toArray());
  }
}

See this stackblitz example

Community
  • 1
  • 1
Elias Faraone
  • 266
  • 2
  • 7