Post contents
Article Overview
This article was written with the idea that the reader is at least somewhat familiar with the introductory concepts of Angular. If you're new to Angular, I might suggest checking out my free book, which teached Angular in-depth called "The Framework Field Guide".
Also, despite the prevalence of Control Flow Syntax (
@for,@if, et al), we're using structural directives for conditional logic (*ngIf,*ngFor, et al) for educational reasons here.
One of the core concepts to the Angular framework is the idea of templates. Templates allow developers to create embedded views of UI from other locations.
These templates not only power many of Angular's baseline features, but are extremely versatile in their capabilities and serve as powerful tools to leverage:
- Templates can be passed and called manually in a similar way to functions.
- You can leverage a set of APIs built into these templates to pass and manipulate data from one template to another during the render process
While this article is far from a comprehensive list of all template related APIs, I want to walk through as much as I can to help you understand how templates work in Angular, what you're able to do with them, and loosely how they're used within Angular itself. Some of the APIs we'll be going through include:
ng-templateTemplateRefEmbeddedViewRefViewContent/ViewChildrenViewContainerRefcreateEmbeddedView- Structural Directives (such as
*ngIf)
By the end of this article, you'll not only have read some of Angular's source code (as of 19.0.0), but you should have a better understanding of how to implement many of these tools and how some of the APIs you use daily work under-the-hood.
It's going to be a long article, so please feel free to take breaks, grab a drink to enjoy while reading, pause to tinker with code, or anything in-between. Feedback is always welcomed and appreciated.
Sound like a fun time? Let's goooo! 🏃🌈
The contents of this post was also presented in a talk under the same name. You can find the slides here or a live recording of that talk given by the post's author on our YouTube channel.
Introduction To Templates
ng-template
Before we dive into the meat of this article, let's do a quick recap of what templates are and what they look like.
While Angular templates come in many shapes and sizes, a simple but common use for them might look something like this:
<ng-template #falseTemp> <p>False</p></ng-template><p *ngIf="bool; else falseTemp">True</p>Start To Source 1 Ng Template - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component } from "@angular/core";import { NgIf } from "@angular/common";@Component({ selector: "my-app", imports: [NgIf], template: ` <ng-template #falseTemp> <p>False</p> </ng-template> <p *ngIf="bool; else falseTemp">True</p> `,})export class AppComponent { bool = false;}bootstrapApplication(AppComponent);In this example, we are creating a template and assigning it to a template reference variable. This template reference variable makes falseTemp a valid variable to use as a value for other inputs in the same template. It then handles that variable similarly to how a variable from the component logic is handled when referenced from the template.
We are then adding the ngIf structural directive to the paragraph tag to render content to the screen conditionally.
- If
boolis true, it renders<p>True</p>, and the template containing<p>False</p>does not - If
boolis false, it then checks if theelsecondition built intongIfhas a value assigned to it. If there is a value assigned to theelsecondition, it renders that template.- In this example, it does; the template we've assigned to
templHere. Because of this,<p>False</p>is rendered
- In this example, it does; the template we've assigned to
If you had forgotten to include the ngIf, it would never render the False element because a template is not rendered to the view unless explicitly told to — this includes templates created with ng-template
Rendering Manually with ngTemplateOutlet
But there's a simpler much more complex another way show the same template code above!
<ng-template #falseTemp> <p>False</p></ng-template><ng-template #ifTrueCondTempl> <p>True</p></ng-template><ng-template [ngTemplateOutlet]="bool ? ifTrueCondTempl : falseTemp"></ng-template>Start To Source 2 Conditional Render - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component } from "@angular/core";import { NgTemplateOutlet } from "@angular/common";@Component({ selector: "my-app", imports: [NgTemplateOutlet], template: ` <ng-template #falseTemp> <p>False</p> </ng-template> <ng-template #ifTrueCondTempl> <p>True</p> </ng-template> <ng-template [ngTemplateOutlet]="bool ? ifTrueCondTempl : falseTemp" ></ng-template> `,})export class AppComponent { bool = true;}bootstrapApplication(AppComponent);While this is not how the
ngIfstructural template works internally, this is a good introduction to thengTemplateOutletdirective, which adds functionality to theng-templatetag.If you're curious to how Angular's
ngIfworks, read on dear reader.
While I'd mentioned previously that ng-template does not render to the DOM, because we're using ngTemplateOutlet, it renders the template defined in the passed ng-template.
This template that's defined by ng-template is called a "view", and when it is rendered to the screen, it is called an "embedded view".
This embedded view is located in the DOM, where the ng-template that used the ngTemplateOutlet resides. That is to say, if you look at the element inspector, the element is placed where you'd expect the ng-template to be located based on the structure of your code.
Knowing that, you can see that the following example would show the user three of the most mythical beasts imaginable:
<ng-template #unicorns><button>🦄🦄🦄</button></ng-template><ng-template [ngTemplateOutlet]="unicorns"></ng-template>
With this, combined with template reference variables, you may find it easier to use a ternary operator to pass the correct template based on the value of bool to create an embedded view of that template.
Pass Data To Templates — The Template Context
Do you know how I mentioned that you can pass data between templates (at the start of the article)? This can be accomplished by defining the context of the template. This context is defined by a JavaScript object you pass to the template with your desired key/value pairs (just like any other object). When looking at an example below, think of it in terms of passing data from a parent component to a child component through property binding. When you define the context of a template, you're simply giving it the data it needs to fulfill its purpose in much the same way.
So, now that we know what they are in broad terms, what do they look like?
While we used the ngTemplateOutlet directive before to render a template, we can also pass an input to the directive ngTemplateOutletContext to pass a context. A context is just an object with a standard key/value pairing.
<ng-template [ngTemplateOutlet]="showMsgToPerson" [ngTemplateOutletContext]="{$implicit: 'Hello World', personName: 'Corbin'}"></ng-template>
From there, you can use let declarations to create template variables in that template based on the values passed by the context like so:
<ng-template #showMsgToPerson let-message let-thisPersonsName="personName"> <p>{{message}} {{thisPersonsName}}</p></ng-template>
Here, you can see that let-templateVariableName="contextKeyName" is the syntax to bind any named context key's value to the template input variable with the name you provided after let. There is an edge-case you've probably noticed though, the $implicit key of the context is treated as a default of sorts, allowing a user to simply leave let-templateVariableName to be the value of the $implicit key of the context value.
Now let's see it in action!
Start To Source 3 Context - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component } from "@angular/core";import { NgTemplateOutlet } from "@angular/common";@Component({ selector: "my-app", imports: [NgTemplateOutlet], template: ` <ng-template [ngTemplateOutlet]="showMsgToPerson" [ngTemplateOutletContext]="{ $implicit: 'Hello World', personName: 'Corbin', }" > </ng-template> <ng-template #showMsgToPerson let-message let-thisPersonsName="personName"> <p>{{ message }} {{ thisPersonsName }}</p> </ng-template> `,})export class AppComponent {}bootstrapApplication(AppComponent);As a quick note, I only named these template input variables differently from the context value key to make it clear that you may do so. let-personName="personName" is not only valid, but it also can make the code's intentions clearer to other developers.
View References — ViewChild/ContentChild
Keeping Logic In Your Controller using ViewChild
While template reference variables are very useful for referencing values within the template itself, there may be times when you'll want to access a reference to an item in the template from the component logic. Luckily, there's a way to get a reference to any component, directive, or view within a component template.
Using ViewChild, you're able to grab a reference to the ng-template from the component logic rather than the template code:
import { NgTemplateOutlet } from "@angular/common";@Component({ selector: 'my-app', imports: [NgTemplateOutlet], template: ` <div> <ng-template #helloMsg>Hello</ng-template> </div> <ng-template [ngTemplateOutlet]="helloMessageTemplate"></ng-template> `})export class AppComponent { // Ignore the `static` prop for now, we'll cover that in just a bit @ViewChild('helloMsg', {static: false}) helloMessageTemplate: TemplateRef<any>;}Start To Source 4 Viewchild - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component, ViewChild, TemplateRef } from "@angular/core";import { NgTemplateOutlet } from "@angular/common";@Component({ selector: "my-app", imports: [NgTemplateOutlet], template: ` <div> <ng-template #helloMsg>Hello</ng-template> </div> <ng-template [ngTemplateOutlet]="helloMessageTemplate"></ng-template> `,})export class AppComponent { @ViewChild("helloMsg", { static: false }) helloMessageTemplate!: TemplateRef<any>;}bootstrapApplication(AppComponent);While this example is effectively not-much-more than an alternative API to
ngTemplateOutlet, it serves as a basis for introducing into further concepts.
ViewChild is a "property decorator" utility for Angular that searches the component tree to find what you pass it as a query. In the example above, when we pass the string 'templName', we are looking for something in the tree that is marked with the template variable helloMsg. In this case, it's an ng-template, which is then stored to the helloMessageTemplate property when this is found. Because it is a reference to a template, we are typing it as TemplateRef<any> to have TypeScript understand the typings whenever it sees this variable.
Not Just for Templates!
ViewChild isn't just for templates, either. You can get references to anything in the view tree:
@Component({ selector: 'my-app', imports: [MyComponentComponent], template: ` <my-custom-component #myComponent [inputHere]="50" data-unrelatedAttr="Hi there!"></my-custom-component> `})export class AppComponent { @ViewChild('myComponent', {static: false}) myComponent: MyComponentComponent;}
For example, would give you a reference to the MyComponentComponent instance of the template. If you ran:
/* This would be added to the `AfterViewInit` lifecycle method */console.log(this.myComponent.inputHere); // This will print `50`
It would give you the property value on the instance of that component. Angular by default does a pretty good job at figuring out what it is that you wanted to get a reference of and returning the "correct" object for that thing.
Start To Source 5 View Not Template - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { AfterViewInit, Component, Input, ViewChild } from "@angular/core";@Component({ selector: "my-custom-component", template: ` <p>Check the console to see the inputs' values from the parent component</p> `,})export class MyComponentComponent { @Input() inputHere!: number;}@Component({ selector: "my-app", imports: [MyComponentComponent], template: ` <my-custom-component #myComponent [inputHere]="50" data-unrelatedAttr="Hi there!" ></my-custom-component> `,})export class AppComponent implements AfterViewInit { @ViewChild("myComponent", { static: false }) myComponent!: MyComponentComponent; ngAfterViewInit() { console.log(this.myComponent.inputHere); // This will print `50` }}bootstrapApplication(AppComponent);Despite the examples thus far having only used a string as the query for ViewChild, you're also able to use the ComponentClass to query for a component with that component type.
/* This would replace the previous @ViewChild */@ViewChild(MyComponentComponent) myComponent: MyComponentComponent;
For the particular example listed above, this code change would still yield the same results. When using ViewChild, it might be dangerous to do this if you have many components with that class. This is because when using ViewChild, it only returns the first result that Angular can find — this could return results that are unexpected if you're not aware of that.
My Name is Inigo Montoya the read Prop
Awesome! But I wanted to get the value of the data-unrelatedAttr attribute dataset, and my component definition doesn't have an input for that. How do I get the dataset value?
Ahh, so you've seen the problem with Angular's guessing of what datatype you're looking for. There are times where we, the developers, know better of what we're looking for than the framework services.
Fancy that.
When we want to overwrite the type of data we expect ViewChild to return, we can use a second property passed to the ViewChild decorator with the type we want to be returned. With the use-case mentioned above, we can tell Angular that we want a reference to the element of the component itself by using the ElementRef.
/* This would replace the previous @ViewChild */@ViewChild('myComponent', {read: ElementRef, static: false}) myComponent: ElementRef;
Now that we've configured the ViewChild to read this as an ElementRef (a class provided from @angular/core which helps us get the right value back from the query) rather than a component reference, we're able to use the nativeElement property of that class to get the HTMLElement object for that component instance.
/* This would be added to the `AfterViewInit` lifecycle method */console.log(myComponent.nativeElement.dataset.getAttribute('data-unrelatedAttr')); // This output `"Hi there!"`Start To Source 6 Read Prop - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { AfterViewInit, Component, ElementRef, Input, ViewChild,} from "@angular/core";@Component({ selector: "my-custom-component", template: ` <p>Check the console to see the inputs' values from the parent component</p> `,})export class MyComponentComponent { @Input() inputHere!: number;}@Component({ selector: "my-app", imports: [MyComponentComponent], template: ` <my-custom-component #myComponent [inputHere]="50" data-unrelatedAttr="Hi there!" ></my-custom-component> `,})export class AppComponent implements AfterViewInit { @ViewChild("myComponent", { read: ElementRef, static: false }) myComponent!: ElementRef; ngAfterViewInit() { console.log( this.myComponent.nativeElement.getAttribute("data-unrelatedAttr"), ); // This output `"Hi there!"` }}bootstrapApplication(AppComponent);ViewChild isn't an only child, though (get it?). There are other APIs similar to it that allow you to get references to other items in your templates from your component logic.
ViewChildren: More references then your nerdy pop culture friend
ViewChildren allows you to get a reference to any items in the view that match your ViewChildren query as an array of each item that matches:
@Component({ selector: 'my-app', imports: [MyComponentComponent], template: ` <div> <my-custom-component [inputHere]="50"></my-custom-component> <my-custom-component [inputHere]="80"></my-custom-component> </div> `})export class AppComponent { @ViewChildren(MyComponentComponent) myComponents: QueryList<MyComponentComponent>;}Start To Source 7 Viewchildren - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { AfterViewInit, Component, Input, QueryList, ViewChildren,} from "@angular/core";@Component({ selector: "my-custom-component", template: ` <p>I am a my-custom-component!</p> `,})export class MyComponentComponent { @Input() inputHere!: number;}@Component({ selector: "my-app", imports: [MyComponentComponent], template: ` <div> <p>Check the console to see that values of the two components</p> <my-custom-component [inputHere]="50"></my-custom-component> <my-custom-component [inputHere]="80"></my-custom-component> </div> `,})export class AppComponent implements AfterViewInit { @ViewChildren(MyComponentComponent) myComponents!: QueryList<MyComponentComponent>; ngAfterViewInit() { console.log(this.myComponents.length); // This will output 2 }}bootstrapApplication(AppComponent);Would give you a list of all components with that base class. You're also able to use the {read: ElementRef} property from the ViewChild property decorator to get a QueryList<ElementRef> (to be able to get a reference to the DOM Elements themselves) instead of a query list of MyComponentComponent types.
What is QueryList
While QueryList (from @angular/core) returns an array-like, and the core team has done an outstanding job at adding in all the usual methods (reduce, map, etc.) and it extends an iterator interface (so it works with *ngFor in Angular templates and for (let i of _) in TypeScript/JavaScript logic), it is not an array. A similar situation occurs when using document.querySelectorAll in plain JavaScript. If you're expecting an array from an API that returns QueryList, it might be best to use Array.from on the value (in this case the myComponents component prop) when you access it in logic later.
A QueryList also allows for some nice additions like the changes observable property that allows you to listen for changes to this query. For example, if you had some components that were hidden behind a toggle:
<!-- This would make up the template of a new component --><input type="checkbox" [(ngModel)]="bool"/><div *ngIf="bool"> <my-custom-component></my-custom-component></div><my-custom-component></my-custom-component>
And wanted to get the value of all component's numberProp values reduced into one, you could do so using the changes observable:
/* This would be added to the `AfterViewInit` lifecycle method */this.myComponents.changes.subscribe(compsQueryList => { const componentsNum = compsQueryList.reduce((prev, comp) => { return prev + comp.numberProp; }, 0); console.log(componentsNum); // This would output the combined number from all of the components' `numberProp` fields. This would run any time Angular saw a difference in the values});Start To Source 8 Querylist - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { AfterViewInit, Component, Input, QueryList, ViewChildren,} from "@angular/core";import { NgIf } from "@angular/common";import { FormsModule } from "@angular/forms";@Component({ selector: "my-custom-component", template: ` <p>My value is {{ numberProp }}</p> `,})export class MyComponentComponent { @Input() inputHere!: number; numberProp = Math.floor(Math.random() * 20);}@Component({ selector: "my-app", imports: [MyComponentComponent, NgIf, FormsModule], template: ` <p>The console should output the combination of all values below</p> <input type="checkbox" [(ngModel)]="bool" /> <div *ngIf="bool"> <my-custom-component></my-custom-component> </div> <my-custom-component></my-custom-component> `,})export class AppComponent implements AfterViewInit { @ViewChildren(MyComponentComponent) myComponents!: QueryList<MyComponentComponent>; bool = false; ngAfterViewInit() { this.myComponents.changes.subscribe( (compsQueryList: QueryList<MyComponentComponent>) => { const componentsNum = compsQueryList.reduce((prev, comp) => { return prev + comp.numberProp; }, 0); console.log(componentsNum); // This would output the combined number from all of the component's `numberProp` field. This would run any time Angular saw a difference in the values }, ); }}bootstrapApplication(AppComponent);It might be a good idea to gain familiarity of doing this as the Angular docs give the following warning in the QueryList docs:
NOTE: In the future this class will implement an Observable interface.
ContentChildren: If this article had kids
Author's note:
This section of the article assumes that you know what the
ng-contenttag is. While I could do an in-depth dive on whatng-contentand content projection is, it's somewhat outside of the scope of this current article. Let me know if this is something that interests you; I might do another deep, deep dive into how Angular parses tags likeng-contentand how it's handled by Angular's AST and template parsing/etc.If you're less familiar with
ng-content, you can probably get by with just knowing how parent/child relationships elements work and just reading through carefully. Never be afraid to ask questions!There's also the
:hostselector used in these demos. Think of each component creating their own wrapperdiv— the:hostselector applies styling to the component wrapper element itself.
I always love nesting some of my code into ng-contents. I don't know what's so appealing about having my code look like it's straight out of HTML spec, but just being able to pass component instances and elements as children to one of my components and then tinkering with them is so satisfying.
One thing I always run into though is that I always end up wanting to style the components that are passed in. Take the following example:
<cards-list> <!-- Cards list has default styling with grey background --> <action-card></action-card> <!-- Action card has default styling with grey background --> <action-card></action-card> <!-- It's also widely used across the app, so that can't change --></cards-list>
Anyone with a sense of design might be cringing about now. Grey on grey? On cards? Yuck! Let's make those cards have some white backgrounds.
This might seem like a trivial task to anyone assuming that these components are built-in HTML elements as of course a CSS stylesheet like so would apply:
// cards-list.component.cssaction-card { background: white;}
But this is often not the case. Angular's ViewEncapsulation prevents styles from one component from affecting the styling of another. This is especially true if you're using a configuration that allows the native browser to handle the components under the browser's shadow DOM APIs, which restricts stylesheet sharing on a browser-level. This is why the Angular-specific CSS selector ::ng-deep is considered an anti-pattern from the Angular core team.
It's no matter, though. We have the power of ViewChildren on our side! Corbin already showed us how to get a reference to an element of a rendered component! Let's spin up an example:
@Component({ selector: 'action-card', template: `<div></div>`, styles: [` :host { border: 1px solid black; display: inline-block; height: 300px; width: 100px; background: grey; margin: 10px; } `]})export class ActionCard {}@Component({ selector: 'cards-list', template: `<div><ng-content></ng-content></div>`, styles: [`:host {background: grey; display: block;}`})export class CardsList implements AfterViewInit { @ViewChildren(ActionCard, {read: ElementRef}) actionCards; ngAfterViewInit() { // Any production code should absolutely be cleaning this up properly, // this is just for demonstration purposes this.actionCards.forEach(elRef => { console.log("Changing background of a card"); this.renderer.setStyle(elRef.nativeElement, "background", "white"); }); }}
Awesome, let's spin that up and… Oh.
Start To Source 9 Cardlist Broke - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { AfterViewInit, Component, ElementRef, QueryList, Renderer2, ViewChildren,} from "@angular/core";@Component({ selector: "action-card", template: ` <div>This is a card</div> `, styles: [ ` :host { border: 1px solid black; display: inline-block; height: 300px; width: 100px; background: grey; margin: 10px; } `, ],})export class ActionCard {}@Component({ selector: "cards-list", template: ` <div><ng-content></ng-content></div> `, styles: [ ` :host { background: grey; display: block; } `, ],})export class CardsList implements AfterViewInit { constructor(private renderer: Renderer2) {} @ViewChildren(ActionCard, { read: ElementRef }) actionCards!: QueryList<ElementRef>; ngAfterViewInit() { // Any production code should absolutely be cleaning this up properly, this is just for demonstration purposes this.actionCards.forEach((elRef) => { console.log("Changing background of a card"); this.renderer.setStyle(elRef.nativeElement, "background", "white"); }); }}@Component({ selector: "my-app", imports: [ActionCard, CardsList], template: ` <cards-list> <!-- Cards list has default styling with grey background --> <action-card></action-card> <!-- Action card has default styling with grey background --> <action-card></action-card> <!-- It's also widely used across the app, so that can't change --> </cards-list> `,})export class AppComponent {}bootstrapApplication(AppComponent);The cards are still grey. Let's open up our terminal and see if the console.logs ran.
They didn't.
Alright, I could keep going, but I know you've all read the section title (👀 at the skim-readers).
ViewChildren is a fantastic tool but only works for the items defined in the template of the component itself. Any children that are passed to the component are not handled the same way and require ContentChildren instead. The same applies to ViewChild (which has the adjacent API of ContentChild). The ContentChild/ren should share the same API with their ViewChild/ren counterparts.
If we change the ViewChildren line to read:
@ContentChildren(ActionCard, {read: ElementRef}) actionCards;Start To Source 10 Cardlist Fixed - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { AfterViewInit, Component, ContentChildren, ElementRef, QueryList, Renderer2,} from "@angular/core";@Component({ selector: "action-card", template: ` <div>This is a card</div> `, styles: [ ` :host { border: 1px solid black; display: inline-block; height: 300px; width: 100px; background: grey; margin: 10px; } `, ],})export class ActionCard {}@Component({ selector: "cards-list", template: ` <div><ng-content></ng-content></div> `, styles: [ ` :host { background: grey; display: block; } `, ],})export class CardsList implements AfterViewInit { constructor(private renderer: Renderer2) {} @ContentChildren(ActionCard, { read: ElementRef }) actionCards!: QueryList<ElementRef>; ngAfterViewInit() { // Any production code should absolutely be running `Renderer2` to do this rather than modifying the native element yourself this.actionCards.forEach((elRef) => { console.log("Changing background of a card"); this.renderer.setStyle(elRef.nativeElement, "background", "white"); }); }}@Component({ selector: "my-app", imports: [ActionCard, CardsList], template: ` <cards-list> <!-- Cards list has default styling with grey background --> <action-card></action-card> <!-- Action card has default styling with grey background --> <action-card></action-card> <!-- It's also widely used across the app, so that can't change --> </cards-list> `,})export class AppComponent {}bootstrapApplication(AppComponent);We'll see that the code now runs as expected. The cards are recolored, the consoles.logs ran, and the developers are happy.
The Content Without the ng
ContentChild even works when you're not using ng-content but still passing components and elements as children to the component. So, for example, if you wanted to pass a template as a child but wanted to render it in a very specific way, you could do so:
<!-- root-template.component.html --><render-template-with-name> <ng-template let-userName> <p>Hello there, {{userName}}</p> </ng-template></render-template-with-name>// render-template-with-name.component.ts@Component({ selector: 'render-template-with-name', imports: [NgTemplateOutlet], template: ` <ng-template [ngTemplateOutlet]="contentChildTemplate" [ngTemplateOutletContext]="{$implicit: 'Name here'}"> </ng-template>`})export class AppComponent { @ContentChild(TemplateRef, {static: false}) contentChildTemplate;}
This is a perfect example of where you might want @ContentChild — not only are you unable to use ng-content to render this template without a template reference being passed to an outlet, but you're able to create a context that can pass information to the template being passed as a child.
How Does Angular Track the UI
Awesome! We've been blowing through some of the real-world uses of templates like a bullet-train through a tunnel. 🚆 But I have something to admit: I feel like I've been doing a pretty bad job at explaining the "nitty-gritty" of how this stuff works. While that can often be a bit more dry of a read, I think it's very important to be able to use these APIs to their fullest. As such, let's take a step back and read through some of the more abstract concepts behind them.
One of these abstract concepts comes from how Angular tracks what’s on-screen; just like the browser has the Document Object Model tree (often called the DOM), Angular has the View Hierarchy Tree.
The DOM Tree
Okay, I realize I just dropped some vocab on you without explaining first. Let's change that.
So, when you build out an HTML file, you're defining the shape the document object model (DOM) takes. When you load a file similar to this:
<!-- index.html --><!-- ids are only added for descriptive purposes --><main id="a"> <ul id="b"> <li id="c">Item 1</li> <li id="d">Item 2</li> </ul> <p id="e">Text here</p></main>
The browser takes the items that've been defined in HTML and turns them into a tree that the browser can understand how to layout and draw on the screen. That tree, internally, might look something like this:
This tree tells the browser where to place items and includes some logic when combined with CSS, even. For example, when the following CSS is applied to the index.html file:
#b li { background: red;}
It finds the element with the ID of b, then the children of that tag are colored red. They're "children" because the DOM tree keeps that relationship info that's defined by the HTML.
The
ulelement is marked as green just to showcase that it is the element being marked by the first part of the selector
If you want to have a better grasp on the DOM and how it relates to the content you see on-screen, check out our article that outlines what the DOM is and how your code interfaces with it through the browser.
View Hierarchy Tree
In the same way, the browser keeps track of what's rendered into the dom using the DOM tree, Angular has its own tree to keep track what's rendered on-screen.
The reason Angular has its own tree is due to the dynamic nature of Angular. In order to understand how to hide content on the fly, change out the content on-screen, and know how to keep consistent expected interactions between all of this, Angular needs to have a tree to keep track of its state.
While Angular renders to the DOM in the end (just as vanilla HTML would), Angular has the original information that described how to render things onto screen. When Angular detects changes to this tree, it will then update the DOM with the changes that Angular has tracked.
I will make a note that, while Angular's View Hierarchy Tree is used by Angular to keep track of component/template composition (and some might argue that this is a "virtual DOM" of sorts as it updates the DOM based off of it's own tree), Angular makes no claims that this is a virtual DOM (AFAIK).
Virtual DOMs have highly contested conversation surrounding them and have no standard definition as-to what one is or is not. I only used the DOM to present a foundational understanding of hierarchy trees in general.
Because this tree is used to update the DOM rather than being part of the DOM itself, the tree Angular uses to track its state is called the "view hierarchy tree". This tree is composed of various "views". A view is a grouping of elements and is the smallest grouping of elements that can be created or destroyed together. A view is defined by a template. This template on its own is not a view, but does define a view
Because of this, despite there being many templates — this code sample does not have any views in it, because they are not being created from any of the templates:
<ng-template>I am a view that's defined by a template</ng-template><ng-template> <p>So am I! Just a different one. Everything in THIS template is in the same view</p> <div>Even with me in here? <span>Yup!</span></div></ng-template>
However, when you create a view from a template, you're able to display them on-screen. When a view is displayed on-screen, they're then called an embedded view. So, when we render a template using ngTemplateOutlet, we are creating a view from a template, then embedding the view in the view that you called the ngTemplateOutlet in.
As such, the following code example would create the view hierarchy in the chart below the code sample:
<ng-template> <p>I am in a view right now</p> <ng-template #rememberMsg> But as you might recall, this is also a view </ng-template> <ng-template [ngTemplateOutlet]="rememberMsg" [ngTemplateOutletContext]="{$implicit: 'So when we render it, it\'s a view within a view'}" ></ng-template></ng-template>
The arrow in this chart simply shows that the view is being defined by the template itself
It's this composition of views that make up the "view hierarchy".
View Containers
Admittedly, that chart above isn't QUITE right. A more accurate version of the chart might look something like this:
Little has changed, yet there's something new! A view container is just what it sounds like: It's a container for views. That is to say, whenever you see a view embedded, you can be sure it's a child of a view container. While our code might not make it apparent, when we're using ngTemplateOutlet, Angular creates a view container for us to place the view into. It will create the view container from a template, view, or even from an element.
<p> <ng-template #letsRender> Let's render this thing! </ng-template> <ng-template [ngTemplateOutlet]="letsRender"></ng-template></p>
It is because Angular's view containers being able to be attached to views, templates, and elements that enable the dependency injection system to get a ViewContainerRef regardless of what you're requested the ViewContainerRef on.
Host Views
If you're looking for them, you might notice a few similarities between a component declaration's template and ng-templates:
- Both of them allow for values to be passed into them (
@Inputprops for components, context for templates) - Both of them contain the same support for tags and template creation (using
ng-template).
Well, there's a good reason for that: A component is actually just a directive with a special view — a "host view" (defined by the template or templateUrl field in the decorator) associated with it.
To quote the Angular documentation:
A component is technically a directive. However, components are so distinctive and central to Angular applications that Angular defines the
@Component()decorator, which extends the@Directive()decorator with template-oriented features.
This host view can also be attached to another view by using the selector value of that component's.
@Component({ selector: "child-component", imports: [NgTemplateOutlet], template: ` <p>I am in the host view, which acts as a view container for other views to attach to</p> <div><p>I am still in the child-component's host view</p></div> <ng-template #firstChildCompTempl> <p>I am in a view outside of the child-component's host view</p> </ng-template> <ng-template [ngTemplateOutlet]="firstChildCompTempl" [ngTemplateOutletContext]="{$implicit: 'And now I'm attaching that template to the host view by embedding the view'}" ></ng-template> `})export class ChildComponent {}@Component({ selector: 'my-app', imports: [ChildComponent], template: ` <p>I am in app's host view, and can act as a view container for even other host views by using the component's selector</p> <child-component></child-component> `})export class AppComponent {}
Template Input Variable Scope
Template input variables are the variables you bind to a template when using context. <ng-template let-varName>. These variables are defined from the context that is applied to the template. As a result these templates are able to be accessed by the children views of the templates, but not from a higher level — as the context is not defined above the template:
<!-- ✅ This is perfectly fine --><ng-template let-varName><p>{{varName}}</p></ng-template><!-- ❌ This will throw errors, as the template context is not available from anywhere that isn't a child of the template --><ng-template let-thisVar></ng-template><p>{{thisVar}}</p>
Template Reference Variable Scope
Template reference variables, however, have a much more complex answer in regards to how they're able to be accessed.
As a small review of what they are: A template reference variable is a variable assigned to a tag so that other items in the same template are able to reference that tag.
<div> Hello There! <ng-template #testingMessage><p>Testing 123</p></ng-template></div><ng-template [ngTemplateOutlet]="testingMessage"></ng-template><!-- Will now show the following in the DOM: --><!-- <div>Hello There!</div> --><!-- <p>Hi There</p> -->
In this example, we're getting a reference to testingMessage template to be able to provide as an input. We're then passing that value to another ng-template's ngTemplateOutlet directive to get it rendering on-screen.
Straightforward enough example, let’s see a more difficult example:
<ng-template #helloThereMsg> <p>Hello There!</p> <ng-template #testingMessage> <p>Testing 123</p> </ng-template></ng-template><div> <ng-template [ngTemplateOutlet]="helloThereMsg"></ng-template></div><ng-template [ngTemplateOutlet]="testingMessage"></ng-template>Start To Source 11 Broke Template Var - StackBlitz
Editimport "zone.js";import { bootstrapApplication } from "@angular/platform-browser";import { Component } from "@angular/core";import { NgTemplateOutlet } from "@angular/common";@Component({ selector: "app-root", imports: [NgTemplateOutlet], template: ` <ng-template #helloThereMsg> <p>Hello There!</p> <ng-template #testingMessage> <p>Testing 123</p> </ng-template> </ng-template> <div> <ng-template [ngTemplateOutlet]="helloThereMsg"></ng-template> </div> <ng-template [ngTemplateOutlet]="testingMessage"></ng-template> `,})export class AppComponent {}bootstrapApplication(AppComponent);If you look at the output of this example, you'll notice that testingMessage isn't rendering. This is because template reference variables bind to the view that they're present in; and as a result are unable to be accessed from parent views.
Like how CSS is applied to a dom when bound to a selector, template reference variables can be accessed within the view itself and child views, but not the parent views.
When the view that is trying to render testMessage looks for that template reference variable, it is unable to, as it is bound to the helloThereMsg template view. Because it cannot find a template reference variable with the id testMessage, it treats it like any other unfound variable: an undefined value. The default behavior of undefined being passed to ngTemplateOutlet is to not render anything.
In order to fix this behavior, we'd need to move the second ng-template into the helloThereMsg template view so that the ngTemplateOutlet is able to find the matching template reference variable within its view scope.
<ng-template #helloThereMsg> Hello There! <ng-template #testingMessage><p>Testing 123</p></ng-template> <ng-template [ngTemplateOutlet]="testingMessage"></ng-template></ng-template><div> <ng-template [ngTemplateOutlet]="helloThereMsg"></ng-template></div>