What are practical scenarios of *ngTemplateOutlet directive?

34,248

Solution 1

Angular template outlets can be used to insert a common template in various sections of a view that are not generated by a loop or subject to a condition. For example, you can define a template for the logo of a company and insert it in several places in the page:

<div>
  <ng-container *ngTemplateOutlet="companyLogoTemplate"></ng-container>
  <h1>Company History</h1>
  <div>{{companyHistory}}</div>
</div>
<form (ngSubmit)="onSubmit()">
  <ng-container *ngTemplateOutlet="companyLogoTemplate"></ng-container>
  <h1>User info</h1>
  <label>Name:</label><input type="text" [(ngModel)]="userName" />
  <label>Account ID:</label><input type="text" [(ngModel)]="accountId" />
  <button>Submit</button>
</form>
<div class="footer">
  <ng-container *ngTemplateOutlet="companyLogoTemplate"></ng-container>
</div>

<ng-template #companyLogoTemplate>
  <div class="companyLogo">
    <img [src]="logoSourceUrl">
    <label>The ACME company, {{employeeCount}} people working for you!</label>
  </div>
</ng-template>

Templates and template outlets can also help to make a component configurable. The following example is taken from this article by Angular University.

A tab container component defines a default tab header template, but allows to override it with a custom template defined as an input property. The appropriate template (default or custom) is then inserted in the view with a template outlet:

@Component({
  selector: 'tab-container',
  template: `
    <ng-template #defaultTabButtons>
      <div class="default-tab-buttons">
        ...
      </div>
    </ng-template>
    <ng-container *ngTemplateOutlet="headerTemplate || defaultTabButtons"></ng-container>
    ... rest of tab container component ...
  `
})
export class TabContainerComponent {
    @Input() headerTemplate: TemplateRef<any>; // Custom template provided by parent
}

In the parent component, you define the custom tab header template and pass it to the tab container component:

@Component({
  selector: 'app-root',
  template: `      
    <ng-template #customTabButtons>
      <div class="custom-class">
        <button class="tab-button" (click)="login()">
          {{loginText}}
        </button>
        <button class="tab-button" (click)="signUp()">
          {{signUpText}}
        </button>
      </div>
    </ng-template>
    <tab-container [headerTemplate]="customTabButtons"></tab-container>      
  `
})
export class AppComponent implements OnInit {
  ...
}

You can see another advanced use case in this blog post by alligator.io.

Solution 2

One thing that makes *ngTemplateOutlet more powerful than *ng-content is when you use it with [ngTemplateOutletContext] input. This enables you to create a totally unique template that can use state from within the component.

I have used this to create a select component that is uniquely styled per client, via different templates, but shares exactly the same code. Here is a StackBlitz link and also my article about it on indepth.dev.

One example of a benefit of *ngTemplateOutlets over using *ngIfs is that your component does not need to depend on external packages, i.e an Icon lib, if this is only going to be used by a single client.

Solution 3

You have very valid question. If something can be achieved by simple if or switch case, why should we use *ngTemplateOutlet?

Independent Component

You are getting these thought because you are thinking about one independent Component level. In other words everything condition, templates are in the same component. We can easily select the template on the basis of certain condition.

Library Component

Dynamic Template

When I say Library Component, it means generic reusable component ex Autocompleter or Typeahead etc. These components provides the functional part however they allow the developer to choose their own template as per their needs.

Here is the catch now, these templates doesn't reside the Autocompleter, it comes from its @ContentChild.

ex:

<ng-autocompleter>
   <ng-template #item let-item>{{item.name}}</ng-template>
<ng-autocompleter>

In above example the <ng-template> is being defined the developer later point time and its not the direct part of <ng-autocompleter>.

Template Context

Template context is very important whenever highly configured component is developed. Getting the dynamic template (html) is not good enough to serve the purpose. We need to bind the value to the ng-template. Since ng-template is not in the part of ng-autocompleter we need to pass the context which contains all necessary data to bind.

ex : In above case if you see we declared the item variable by let-item but where is item is coming from. That will be decided by the context given to *ngTemplateOutlet.

One line conclusion If we want to inject the templates which is will be declared in future by someone, I cannot handle this case by *ngIf or *ngSwitch. We need to use *ngTemplateOutlet.

Solution 4

The true power of ngTemplateOutlet can be seen when you need to deal with nested items where you don't know how many levels deep you could go. Doing an ngFor or ngSwitch is cumbersome when dealing with nested arrays, and limited as you always need to know beforehand how many levels deep you want to go:

For example:

If this is my array:

  todo = [
    'Get to work',
    [
      'Get up',
      'Brush teeth',
      'Take a shower',
      'Check e-mail',
      'Walk dog'
    ],
    [
      'Prepare for work',
      'Drive to office',
      'Park car',
      'Work',
      [
        'See Clients',
        'Fill Out Forms',
        'Take Tea Breaks',
        [
          'Switch on Kettle',
          'Get Clean Cup',
          'Add Tea',
          'Pour Water',
          'Drink'
        ]
      ]
    ],
    'Pick up groceries',
    'Go home',
    'Fall asleep'
  ];

Then you could use the following approach:

<div *ngFor="let item of todo" style="margin-left: 25px">

  <div *ngIf="!isArray(item); else arrayView">{{item}}</div>
  <ng-template #arrayView>

    <div *ngFor="let item2 of item" style="margin-left: 25px">

      <p>And So On...</p>

    </div>

  </ng-template>

</div>

But what if you don't know how many levels deep your array goes? You could use ngTemplateOutlet in conjunction with an ng-template instead, to take care of the problem:

<ng-template #tmplItems let-todo="todo">

  <div *ngFor="let item of todo" style="margin-left: 25px">

    <div *ngIf="!isArray(item); else arrayView">{{item}}</div>
    <ng-template #arrayView>

      <ng-container *ngTemplateOutlet="tmplItems,context:{todo:item}">

      </ng-container>

    </ng-template>

  </div>

</ng-template>

<div *ngFor="let item of todo">

  <div *ngIf="!isArray(item); else arrayView">{{item}}</div>
  <ng-template #arrayView>

    <ng-container *ngTemplateOutlet="tmplItems,context:{todo:item}">

    </ng-container>

  </ng-template>

</div>

Now it doesn't matter how many levels deep your array goes, calling your ng-template recursively takes care of the problem (unless there is a limitation that I'm not aware of yet).

Output:

Get to work
    Get up
    Brush teeth
    Take a shower
    Check e-mail
    Walk dog
    Prepare for work
    Drive to office
    Park car
    Work
        See Clients
        Fill Out Forms
        Take Tea Breaks
            Switch on Kettle
            Get Clean Cup
            Add Tea
            Pour Water
            Drink
Pick up groceries
Go home
Fall asleep
Share:
34,248
patrick.1729
Author by

patrick.1729

Updated on April 25, 2021

Comments

  • patrick.1729
    patrick.1729 about 3 years

    I was reading about *ngTemplateOutlet directive. The use of this directive is to instantiate a template dynamically by a template reference and context object as parameters.

    What I want to know is that we have so many things in Angular to achieve the same results as *ngTemplateOutlet such as:

    1. We can have multiple *ngIf which could render different templates based on the component variable value within the same component. In a similar fashion we have [ngSwitch] which would render different templates for us based on different values.

    2. We could use references with *ngIf by referring to the template reference variable of the respective variable.

    For the former case:

    <div *ngIf="condition1"> Content 1 </div>
    <div *ngIf="condition2"> Content 2 </div>
    <div *ngIf="condition3"> Content 3 </div>
    

    And for latter:

    <ng-container *ngIf="condition then myTemplate else otherTemplate"></ng-container>
    <ng-template #myTemplate> Some content... </ng-template>
    <ng-template #otherTemplate> Some content... </ng-template>
    

    If we have such methods in our arsenal what more value does *ngTemplateOutlet add?

    What are the practical use cases (if there are any) where we cannot use the above methods and should use *ngTemplateOutlet directive or is it just another method to choose from to achieve the same result?

  • patrick.1729
    patrick.1729 over 5 years
    How would you differentiate the approach of using *ngTemplateOutlet for your latter example of tabs from the approach of content projection in Angular? I mean I could achieve the same result through Content Projection as well.
  • ConnorsFan
    ConnorsFan over 5 years
    For a single template, that is probably true. In cases where several parts of the components are configurable (e.g. header, content, footer), you would have to use templates, since you can provide only one content.
  • patrick.1729
    patrick.1729 over 5 years
    But even in single component I can provide the header, content, footer and then use 'select' property of ng-content to place them accordingly. Then where do they differ?
  • ConnorsFan
    ConnorsFan over 5 years
    How would you implement the example above with ng-content and select? How would you display the default header template if the header part of the projected content is not provided?
  • ConnorsFan
    ConnorsFan over 5 years
    However, I must say that the condition could be implemented with ngIf instead of ngTemplateOutlet.
  • patrick.1729
    patrick.1729 over 5 years
    To answer your first question: <ng-content select="header"></ng-content>. And what conditions are you talking about in your last comment?
  • ConnorsFan
    ConnorsFan over 5 years
    The condition to decide which template is shown: the default header template or the one provided by the parent component. In the code example, the tab component defines a default header template (defaultTabButtons).
  • patrick.1729
    patrick.1729 over 5 years
    Yeah that can easily be achieved by template reference in*ngIf just as I mentioned in my question. But still what is our conclusion to my question of implementing the tab example with ng-content. Are you satisfied that we could achieve it through ng-content or are there any edge cases that I might have overlooked?
  • ConnorsFan
    ConnorsFan over 5 years
    I still don't see how you would implement the example above with ng-content, taking the default template into account. I don't know how to include the select part of the content in a condition.
  • patrick.1729
    patrick.1729 over 5 years
    <ng-container *ngIf="headerTemplate else defaultTemplate"><ng-content select="header"></ng-content></ng-container><ng-template #defaultTemplate> Some content... </ng-template>. How about this one?
  • patrick.1729
    patrick.1729 over 5 years
    No that is just the condition to check whether we have received the input property or not and we can have a default template for the else condition. More so we can do <ng-template #defaultTemplate> <ng-content></ng-content> </ng-template> to render the content that is coming without the header template.
  • ConnorsFan
    ConnorsFan over 5 years
    How do you test if the header select part of the content was provided?
  • patrick.1729
    patrick.1729 over 5 years
    We could not check that in both of the methods neither in *ngTemplateOutlet nor with ng-content. This is the same as asking to check whether the <div> with click action "login" was provided (in your tab example). What we can do in this case is to have our default <ng-content> to render the content as is.
  • ConnorsFan
    ConnorsFan over 5 years
    That eliminates the ng-content implementation if we want to provide a default template for the header. With the template implementation, we can test if the input template was provided or nor. The objection that remains is that it could be implemented with ngIf instead of ngTemplateOutlet.
  • patrick.1729
    patrick.1729 over 5 years
    By "header" I am not referring to the "headerTemplate" input property that you took in your eg. I am referring to the <header> tag that might be present in your "headerTemplate". As you have mentioned: "For a single template, that is probably true. In cases where several parts of the components are configurable (e.g. header, content, footer)..." I was referring this "header" :)
  • ConnorsFan
    ConnorsFan over 5 years
    In short, the presence of the input template can be tested, the presence of the ng-content header part cannot.
  • patrick.1729
    patrick.1729 over 5 years
    Yes, that's my point! So for your second example, I believe we can achieve the same result with ng-content. What would you suggest?
  • ConnorsFan
    ConnorsFan over 5 years
    The ng-content implementation doesn't work, because you don't know if the default header template must be shown or not.
  • patrick.1729
    patrick.1729 over 5 years
    It will be shown only if the "headerTemplate" is not provided as in the case with *ngTemplateOutlet. And I have already depicted how we could achieve it.
  • ConnorsFan
    ConnorsFan over 5 years
    What is headerTemplate (in the context of an ng-content implementation)?
  • patrick.1729
    patrick.1729 over 5 years
    I am just sticking to your tab example where you are using it as input property :)
  • ConnorsFan
    ConnorsFan over 5 years
    But we are discussing your suggestion of replacing that headerTemplate input property with an ng-content implementation, in which case the input property does not exist anymore.
  • patrick.1729
    patrick.1729 over 5 years
    Oh my bad... that's true we cannot use ng-content implementation for conditionally rendering the default template
  • Drenai
    Drenai about 5 years
    The Alligator.io link! That example of using structural directive to turn the elements into TemplateRefs and inject via @ContentChild is unreal clever👍👍
  • anonymous
    anonymous over 3 years
    I am a big fan of you. Your talk about template outlet is really cool. may be you can answer the below question stackoverflow.com/questions/64226242/…
  • Stephen Cooper
    Stephen Cooper over 3 years
    Glad you like the talk! :) I have posted an answer to your question.
  • Balachandran
    Balachandran over 3 years
    I am also big fan because of your stackblitz link. I give two votes for your answer :)
  • Code
    Code about 3 years
    ngTemplateOutletContext is no more in angular 11, how to inject variable in child component?
  • Stephen Cooper
    Stephen Cooper about 3 years
    ngTemplateOutletContext is still in angular 11. The StackBlitz link above is using Angular 11 with it.