This piece assumes you’ve already gotten a handle on Angular’s basics. If that’s not the case yet, it’s strongly recommended to work through the excellent Angular getting started guide first.
At the heart of Angular lies the notion of templates. They let you define embedded UI views that can be reused across different parts of your application.
These templates are not just fundamental to many of Angular’s core features; they’re also remarkably flexible and can be a potent asset:
- You can pass templates around and invoke them manually, much like functions.
- There’s a suite of template-specific APIs you can use to transfer and modify data between templates while rendering.
This article doesn’t aim to cover every template-related API out there, but I’ll guide you through as much as possible so you can grasp how templates function in Angular, what you can accomplish with them, and a rough idea of how Angular itself employs them. Here’s a sample of what we’ll explore:
ng-templateTemplateRefEmbeddedViewRef-
ViewContent/ViewChildren ViewContainerRefcreateEmbeddedView-
Structural Directives (like
*ngIf)
When you reach the end, you’ll have peeked into Angular’s source code (as of version 8.0.1) and gained a clearer understanding of how to apply these tools and what goes on behind the scenes with everyday APIs.
It’s a hefty read, so don’t hesitate to pause whenever you like—grab a beverage, experiment with some code, or just take a breather. Your feedback is always welcome and valued.
Ready for the adventure? Let’s dive in! 🏃🌈
This post’s material was also shared in a talk with the same title. You can access the slides here or watch a live recording of the presentation by the author on our YouTube channel.
ng-template
Before getting into the core topics, let’s briefly review what templates are and how they appear in practice.
Angular templates can take various forms, but a typical, simple example might resemble this:
<ng-template #falseTemp>
<p>False</p>
</ng-template>
<p *ngIf="bool; else falseTemp">True</p>
Here, we build a template and tie it to a template reference variable. By doing this, falseTemp becomes a legitimate value that can be handed to other inputs within the same template. Once referenced, Angular treats it just like any variable coming from the component's logical layer.
Next, we attach the ngIf structural directive to the paragraph element, letting us decide which content becomes visible on screen.
- When
boolevaluates to true,<p>True</p>shows up, while the template holding<p>False</p>stays hidden - When
boolis false, Angular looks at theelsebranch ofngIfto see if a template has been provided. If so, that alternative template is what gets rendered.- In our case, it does—the template we linked via
templHere. Consequently,<p>False</p>becomes visible
- In our case, it does—the template we linked via
Without the ngIf directive, the False content would never appear because a template only enters the view when you explicitly ask for it to—and that includes anything wrapped in ng-template
Manual Rendering via ngTemplateOutlet
Yet there's easier far trickier an alternative approach to display the exact template from 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>
Check out this live demo on StackBlitz
Note that this isn’t how Angular’s
ngIfstructural directive is implemented under the hood, but it serves as a solid entry point for understandingngTemplateOutlet, a directive that enhances the capabilities ofng-template.For a deeper dive into Angular’s actual
ngIfmechanics, keep reading.
Earlier I noted that ng-template doesn’t appear in the DOM by itself. However, with ngTemplateOutlet in play, the content defined inside the associated ng-template is actually rendered.
This content, wrapped within ng-template, is referred to as a “view”; once it’s displayed on the page, it becomes an “embedded view”.
That embedded view shows up in the DOM right where its parent ng-template—the one leveraging ngTemplateOutlet—is positioned. In other words, when you inspect the elements, you’ll find the output exactly where the ng-template sits in your code’s layout.
Bearing that in mind, the following example would present the user with three of the most legendary creatures ever imagined:
<ng-template #unicorns><button>🦄🦄🦄</button></ng-template>
<ng-template [ngTemplateOutlet]="unicorns"></ng-template>
When you pair this approach with template reference variables, reaching for a ternary operator might make life easier — you can pick which template to hand over depending on the current bool value, and then render it as an embedded view.
Pass Data To Templates — The Template Context
Remember the earlier point about passing data between templates? You can achieve that by setting the template's context. To do so, you provide a JavaScript object—complete with the key/value pairs you need—just like any other object. Look at the example coming up, and imagine this as property binding between a parent and a child component. By defining the context, you're handing the template exactly the data it requires to do its job, mirroring that same parent-child pattern.
Okay, so we have a big-picture idea. What does the actual syntax look like?
Earlier, we brought in the ngTemplateOutlet directive to display a template. Now, we can also feed it an input via ngTemplateOutletContext to supply a context. That context is nothing more than a standard object with key/value pairs.
<ng-template
[ngTemplateOutlet]="showMsgToPerson"
[ngTemplateOutletContext]="{$implicit: 'Hello World', personName: 'Corbin'}"
>
</ng-template>
Once you’re inside that template, let declarations let you set up template variables, pulling their values directly from the context provided:
<ng-template #showMsgToPerson let-message let-thisPersonsName="personName">
<p>{{message}} {{thisPersonsName}}</p>
</ng-template>
As demonstrated above, the syntax let-templateVariableName="contextKeyName" binds the value of any given named context key to a template input variable named after let. However, you've likely spotted a special case: the $implicit context key acts as a fallback, so omitting the part after the equals sign and writing let-templateVariableName will simply yield the value stored under $implicit.
Time to try it out!
Check out the live example on StackBlitz
One quick observation: I deliberately gave these template input variables distinct names from their context value keys to highlight the flexibility you have in naming. Using let-personName="personName" is perfectly acceptable and, in fact, often improves code readability for your teammates.
Keeping Logic In Your Controller using ViewChild
Template reference variables come in handy when you need to access values straight inside the template, but situations arise where the component logic itself needs a pointer to something rendered in the view. Fortunately, Angular provides a mechanism to retrieve a reference to any component, directive, or view that exists within a component's template.
With ViewChild, you can pull a reference to the ng-template directly from your component's TypeScript logic, bypassing the template altogether:
@Component({
selector: 'my-app',
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>;
}
Although this example is essentially just a different way to use
ngTemplateOutlet, it lays the groundwork for understanding more complex ideas.
ViewChild acts as a "property decorator" in Angular, which traverses the component tree to locate whatever you specify in its query. In the provided snippet, we supply the string 'templName', meaning we're searching for an element tagged with the template variable helloMsg. Here, that turns out to be an ng-template, and once found, it gets assigned to the helloMessageTemplate property. Since this property holds a template reference, we declare its type as TemplateRef<any> so that TypeScript can properly handle its usage throughout the code.
Beyond Templates!
ViewChild is not limited to templates—you can also grab references to any other element within the view tree.
@Component({
selector: 'my-app',
template: `
<my-custom-component #myComponent [inputHere]="50" data-unrelatedAttr="Hi there!"></my-custom-component>
`
})
export class AppComponent {
@ViewChild('myComponent', {static: false}) myComponent: MyComponentComponent;
}
Running this, for instance, returns a handle to the MyComponentComponent instance tied to the template. Executing the following:
/* This would be added to the `AfterViewInit` lifecycle method */
console.log(this.myComponent.inputHere); // This will print `50`
When you access the property this way, you get the value straight off that component's instance. Angular normally handles the resolution well on its own, determining which object you intended to retrieve and giving you the appropriate reference back.
See this example on StackBlitz
Up to this point, the examples have passed only a string into ViewChild for the query, but you can also pass the ComponentClass, allowing you to query for a component of that specific type.
/* This would replace the previous @ViewChild */
@ViewChild(MyComponentComponent) myComponent: MyComponentComponent;
The code change above would still produce identical output for the given example. That said, relying on ViewChild in this manner can be risky when your app includes numerous components of the same class. The reason? ViewChild grabs only the very first match Angular encounters — a behavior that can lead to surprising references if you aren’t watching for it.
Call Me Inigo Montoya the read Prop
Great — but my actual goal is to pull the value stored in the data-unrelatedAttr dataset, and there’s no corresponding input declared inside my component class. So how can I access that dataset value?
Right, you’ve just run into Angular’s issue with trying to auto-detect the datatype you need. There are moments when we, the developers, have a clearer sense of what we’re after than the framework's internal services.
Imagine that.
To override the type of data that ViewChild returns, you pass a second argument to the ViewChild decorator — which specifies the type you want back. For the scenario described above, we can request a direct reference to the component’s own element via ElementRef.
/* This would replace the previous @ViewChild */
@ViewChild('myComponent', {read: ElementRef, static: false}) myComponent: ElementRef;
With the ViewChild set up to return an ElementRef (an @angular/core-exported class that ensures the query yields the expected type) instead of a component instance, we can access the underlying HTMLElement via the nativeElement property on that class for the given component.
/* This would be added to the `AfterViewInit` lifecycle method */
console.log(myComponent.nativeElement.dataset.getAttribute('data-unrelatedAttr')); // This output `"Hi there!"`
See this example on StackBlitz
But ViewChild isn't the only option available (pun intended). Several other APIs work in a similar fashion, letting you pull references to different template elements straight into your component class.
ViewChildren: More references then your nerdy pop culture friend
With ViewChildren, you can grab references to every view item that matches your query, and the result comes back as an array containing each matching element:
@Component({
selector: 'my-app',
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>;
}
See this example on StackBlitz
From that, you'd obtain every component that derives from that base type. Besides that, the ViewChild property decorator supports a {read: ElementRef} option, which lets you fetch a QueryList<ElementRef> instead of a list of MyComponentComponent instances—so you can access the raw DOM Elements themselves.
What is QueryList
The QueryList from @angular/core behaves like an array—the core team has thoroughly equipped it with typical methods (reduce, map, and so on) and it even implements an iterator, so it functions seamlessly with *ngFor in templates and for (let i of _) in TypeScript or JavaScript code. Yet, it's still not a genuine array. This mirrors how document.querySelectorAll works in vanilla JavaScript. If you thought an API would hand back an actual array but it returns a QueryList instead, the safest move is to wrap it with Array.from (here, that would be myComponents) at the moment you interact with it in your code.
With a QueryList, you also get extras like the changes observable—a handy way to react whenever that query's results shift. Say, for instance, some components were only reachable through 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>
If the goal were to collect every numberProp value from the component into a single result, the changes observable would make that possible:
/* 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
});
If you want to experiment with this, check out the StackBlitz demo.
Getting comfortable with this pattern is worthwhile, since the official docs include a heads-up in the QueryList documentation:
NOTE: In the future this class will implement an Observable interface.
ContentChildren: If this article had kids
A quick word from the author:
Before we dive in, this part presumes you're already familiar with the
ng-contentelement. A full walkthrough of content projection and how Angular's parser handlesng-contentin its AST would be fascinating, but it's not what we're covering right now. If you'd like to see that, drop a comment and I might write a separate, thorough exploration of that topic.Even if
ng-contentisn't second nature to you, you can still follow along — just keep in mind how parent and child elements relate to each other, and read carefully. Don't hesitate to ask if something isn't clear!Also, these examples use the
:hostselector. Picture each component sitting inside its own wrapperdiv— the:hostselector targets that wrapper element itself for styling.
There's just something about tucking nested code inside ng-content elements that I find irresistible. The idea of making my markup look like it belongs in the official HTML spec is oddly appealing — I love handing off component instances and elements as children to my components and then playing around with them.
That said, I frequently hit the same snag: I want to style those passed-in components. Look at this 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>
Design-minded folks will likely find this palette hard to swallow. Gray over gray, sitting on cards? Not exactly pleasing. It's time to give those cards clean white surfaces instead.
For anyone who assumes these components are plain HTML elements, this adjustment looks almost too easy — a stylesheet with rules like the one below should do the trick:
// cards-list.component.css
action-card {
background: white;
}
In reality, though, that assumption usually falls apart. Angular's ViewEncapsulation ensures that styles defined in one component don't leak into another. This becomes especially relevant when you opt for a setup where the native browser takes charge of component encapsulation through the shadow DOM APIs, effectively cutting off stylesheet sharing at the browser layer. That's the reasoning behind the Angular-specific selector ::ng-deep being flagged for depreciation (a tough blow for veteran Angular devs, myself included — a huge migration headache 😭).
But no need to fret, because ViewChildren saves the day! Corbin has already walked us through grabbing a reference to a rendered component's element. Let's put that into action with a quick demo:
@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");
});
}
}
Let's spin that up and… Oh.
See this example on StackBlitz
Still grey cards. Time to check the terminal—did the console.logs fire?
They didn't.
I could keep typing, but I'm sure the skim-readers have already spotted the heading (👀).
ViewChildren is great, yet it only reaches items defined directly in the component's template. Children passed into the component follow a different rule and demand ContentChildren. Similarly, ViewChild has its sibling ContentChild, and both pairs mirror each other's API.
So switching the ViewChildren line to this:
@ContentChildren(ActionCard, {read: ElementRef}) actionCards;
View this example on StackBlitz
This time, the output is exactly what we expect. The card colors update, every consoles.log fires, and the development team is satisfied.
The Content Without ng
ContentChild functions equally well when you skip ng-content entirely yet still hand components or elements to the component as children. Suppose you want a child template but need full control over its rendering—this approach lets you do exactly that:
<!-- 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',
template: `
<ng-template
[ngTemplateOutlet]="contentChildTemplate"
[ngTemplateOutletContext]="{$implicit: 'Name here'}">
</ng-template>
`
})
export class AppComponent {
@ContentChild(TemplateRef, {static: false}) contentChildTemplate;
}
Here, @ContentChild shines — it is not merely about ng-content being unable to display a template without a reference handed to an outlet; it also opens the door to building a context that feeds data into the child-provided template.
Great, we've zoomed through practical template applications at high speed. 🚆 Still, I have a confession: my explanations of the low-level mechanics have been lacking. Dry as they may seem, grasping these fundamentals is key to unlocking the full potential of the APIs. So, let's pause and dig into the deeper theory.
That theory includes Angular's mechanism for monitoring the view; similar to how browsers maintain the Document Object Model tree (that is, the DOM), Angular relies on its own View Hierarchy Tree.
The DOM Tree
Fine, I admit it — I threw that term at you without a proper introduction. Let's fix that now.
When you write an HTML file, you're essentially outlining the structure of the document object model (DOM). Consider loading a file like 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 maps the elements declared in the markup to an internal structure it can interpret for rendering and painting. Under the hood, that structure could resemble the following:
The structure rendered here dictates where each element lands in the browser. Once styles are added, it can even drive conditional presentation. Take this snippet of CSS, which targets index.html:
#b li {
background: red;
}
The element whose ID is b is located first, and after that, all of its child nodes get the red color applied. The term "children" is used because the DOM tree preserves this parent-child connection, which originates from the HTML structure.
The
ulelement is colored green purely to indicate that it is the element matched by the initial section of the selectorTo gain a deeper understanding of the DOM and its connection to what appears on-screen, take a look at our post explaining what the DOM is and how your code communicates with it via the browser.
View Hierarchy Tree
The browser uses the DOM tree to oversee what gets visually output, and Angular mirrors this behavior by maintaining its own tree to monitor what is shown.
Angular requires its own separate tree because of how dynamic it is. To manage runtime hiding and swapping of visible content, while also ensuring consistent, predictable user interactions, Angular depends on a tree structure to store this state.
Although Angular ultimately renders into the DOM (like plain HTML does), Angular also retains the original metadata describing the rendering process. Whenever it notices modifications in this tree, it syncs the DOM to reflect those recorded updates.
It should be mentioned that even though Angular's View Hierarchy Tree handles component and template hierarchies (and some people may call it a "virtual DOM" since it drives DOM updates via its own tree), Angular itself never labels this as a virtual DOM (AFAIK).
The concept of virtual DOMs is heavily disputed and often lacks a clear-cut definition. I referenced the DOM only to build a foundational grasp of how hierarchy trees function.
Since this tree serves to update the DOM without being inside it, the tree Angular uses to manage functionality is called the "view hierarchy tree". That structure is built from "views". A view consists of a pooled set of elements, and it marks the minimal cluster that can be either instantiated or removed as one unit. Each view has a template behind it. That template by itself is not a view, but it does describe what the view will be
As a result, even though templates exist in abundance, this snippet contains no views—since they have yet to be generated from any of those 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>
When a view gets instantiated from a template, it becomes ready for visual rendering. Once that view appears on-screen, it's referred to as an embedded view. Rendering a template through ngTemplateOutlet follows this sequence: a view is derived from the template, then that view is inserted into the host view—the one containing the ngTemplateOutlet token.
This code snippet produces the view hierarchy depicted in the accompanying diagram:
<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>
Within this diagram, the arrow merely indicates that the template defines the view.
The cumulative effect of these individual views is what we term the "view hierarchy".
View Containers
This illustration above, though, isn't exactly precise. A more precise diagram would resemble the following:
A view container does exactly what its name implies—it holds views. In practice, any embedded view you encounter is guaranteed to live inside one. With ngTemplateOutlet, Angular quietly sets up a view container behind the scenes to host the view, even though our code never shows it. This container can originate from a template, a view, or an element.
<p>
<ng-template #letsRender>
Let's render this thing!
</ng-template>
<ng-template [ngTemplateOutlet]="letsRender"></ng-template>
</p>
The reason the dependency injection system can provide a ViewContainerRef for whatever you request it on is that Angular view containers are attachable to views, templates, and elements alike.
Host Views
When you compare a component's template with ng-templates, certain parallels become apparent:
- Both support external value injection (via
@Inputprops on components and context on templates) - Both offer identical tag handling and template generation capabilities, including the use of
ng-template.
This similarity isn't accidental: A component is essentially a directive paired with its own special view — a "host view" (specified through the template or templateUrl property in the decorator).
Here's what the Angular documentation says:
Technically, a component is a directive. However, components stand apart as so unique and integral to Angular applications that Angular introduces the
@Component()decorator, which extends the@Directive()decorator with features related to templates.
This host view is further attachable to another view via that component's selector value.
@Component({
selector: "child-component",
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',
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
When working with context, template input variables are what you bind to a template via <ng-template let-varName>. Their values are determined by the context attached to the template. Hence, child views within that template can reference these variables, while any view higher up in the hierarchy cannot — since the context exists only at the template's level:
<!-- ✅ 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
When it comes to template reference variables, the question of accessibility has a far less straightforward answer.
Here’s a quick reminder of their purpose:
A template reference variable is bound to an element, allowing other parts of the same template to reach that element.
<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> -->
Here, we capture a reference to the testingMessage template, which we then supply as an input. That same value gets forwarded to the ngTemplateOutlet directive attached to another ng-template, causing its contents to appear in the DOM.
That was fairly simple — now for a trickier case:
<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>
See this example on StackBlitz
When you examine what this example produces, testingMessage ends up missing from the rendered output. The reason behind this is that template reference variables are tied to the view where they are declared, which stops parent views from reaching them.
In the same way CSS gets attached to a dom when a selector is applied to it, a template reference variable is visible inside its own view and in child views, but remains out of reach for any parent views.
When the view rendering testMessage attempts to locate that template reference variable, it fails, because the variable is attached to the template view owned by helloThereMsg. As no template reference variable with the id testMessage is found, the lookup falls back to the standard treatment for any missing variable: an undefined value. The way ngTemplateOutlet handles undefined by default is to skip rendering output altogether.
To correct this, the second ng-template would have to be relocated within the helloThereMsg template view, ensuring that ngTemplateOutlet can resolve the desired template reference variable from its own view’s 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>
Check out this example on StackBlitz
Understanding timings with ViewChildren
Yet the behavior of that preceding example doesn't match what we probably had in mind. Our goal was to retrieve:
<div>Hello there!</div>
<p>Testing 123</p>
And instead got:
<div>Hello there! <p>Testing 123</p></div>
The reason behind this behavior is that relocating the template into the proper view scope also shifted its position within the element hierarchy.
Fortunately, we have already discussed @ViewChild, which can traverse the entire view hierarchy to fetch references and supply them to the component code. Since variables defined in the component logic remain reachable from every descendant view of the component's host view, it's possible to elevate the testingMessage template reference variable to the top level.
@Component({
selector: "my-app",
template: `
<ng-template #helloThereMsg>
Hello There!
<ng-template #testingMessage>Testing 123</ng-template>
</ng-template>
<ng-template [ngTemplateOutlet]="helloThereMsg"></ng-template>
<ng-template [ngTemplateOutlet]="testingMessageCompVar"></ng-template>
`
})
export class AppComponent {
@ViewChild("testingMessage", { static: false }) testingMessageCompVar;
}
Opening the console in that example reveals an error message that might ring a bell for anyone with substantial Angular experience under their belt (personally, it’s a error I’ve encountered on numerous occasions!).
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngTemplateOutlet: undefined'. Current value: 'ngTemplateOutlet: [object Object]'.
Angular only surfaces this error while running in developer mode—production builds won't display it.
So why does this error occur, and how can we resolve it?
This is precisely where the interplay between change detection, lifecycle hooks, and the static property becomes relevant.
Change Detection, How Does It Work
Angular's change detection mechanism warrants its own extensive write-up; this isn't that article. Still, grasping how change detection operates and its impact on template readiness is crucial for unraveling some of Angular's more puzzling template behaviors.
For a deeper dive into lifecycle methods and change detection, refer to the official documentation on those topics.
Angular relies on designated moments to refresh the UI. Without these designated moments, it would be impossible for Angular to recognize when on-screen data gets updated. These moments serve as periodic checks for data modifications. Although these inspections aren't perfect, their default behavior covers most scenarios, while still allowing for customization or manual triggering when needed.
One of these standard checks occurs when Angular first begins rendering a component. At this stage, it reviews all values stored in the component's state. Subsequently, it runs similar reviews after any data alteration, deciding whether a UI refresh is necessary.
These reviews invoke the DoCheck lifecycle method, which you have the option to handle yourself. The DoCheck hook fires each time Angular identifies a data change, irrespective of whether that change leads to an on-screen update.
Now, let's revisit the earlier example—but this time, we'll introduce some lifecycle methods to pin down when ViewChild finally hands our value over.
export class AppComponent implements DoCheck, OnChanges, AfterViewInit {
realMsgVar: TemplateRef<any>;
@ViewChild("testingMessage", { static: false }) testingMessageCompVar;
ngOnInit() {
console.log("ngOnInit | The template is present?", !!this.testingMessageCompVar)
}
ngDoCheck() {
console.log("ngDoCheck | The template is present?", !!this.testingMessageCompVar);
this.realMsgVar = this.testingMessageCompVar;
}
ngAfterViewInit() {
console.log('ngAfterViewInit | The template is present?', !!this.testingMessageCompVar);
}
}
See this example on StackBlitz
Once you inspect the console output, you’ll see these logs appear:
ngOnInit | The template is present? false
ngDoCheck | The template is present? false
ngAfterViewInit | The template is present? true
ngDoCheck | The template is present? true
The testingMessageCompVar property only exists once ngAfterViewInit has executed. The root cause of the error lies in the fact that the template doesn't exist in the component's logic until that lifecycle hook fires. This delay is due to how the timing works: the template lives inside an embedded view, and rendering that view to the screen takes a measurable amount of time. In other words, the helloThereMsg template must complete its initial render before the ViewChild decorator can obtain a reference to the child component.
On its own, ViewChild assigns the value to testingMessageCompVar at the exact moment the AfterViewInit lifecycle method executes. That value assignment then propagates back into the template's output.
Angular, however, frowns upon mutating values directly inside AfterViewInit. The framework routinely performs change detection right after ngDoCheck, and once that phase completes, it doesn't want to re-scan the view for pending updates. Re-checking could lead to subtle timing conflicts that stem from how the change detection digest works under the hood — a deep topic that goes far beyond what this post covers.
Consequently, when you rely on ngDoCheck, you're forcing the variable update manually. That manual update signals Angular's change detection to add this modification to its queue of on-screen changes.
I recognize the complexity in this example can be overwhelming — even I find it tricky to write! If you want to dig deeper but feel deflated after going over this part more than once, consider reading this resource (from "Angular University", a solid non-official Angular learning site). It's the reference I used to brush up on the concepts behind this specific error.
Additionally, if you'd like to see me write a full article on Angular change detection, let me know — I'm curious to see if there's an audience for it!
Great Scott — You Control The Timing! The static Prop
Still, there are scenarios where grabbing the value immediately inside ngOnInit would be quite handy. If you're not nesting a view within another view, having the reference before ngAfterViewInit would let you skip the workaround described earlier in this piece.
One quick reminder before we proceed: the
staticprop was added to Angular 8, as seen in this PR. Everything I discuss here doesn't hold forViewChildorContentChildin versions released before that.
You can, in fact, take charge of this timing through the static prop! In all the examples leading up to this one, I've defaulted to static: false to dodge the pitfall outlined in the previous section. But by flipping that flag to true, you gain access to the template reference right inside the ngOnInit lifecycle method:
@Component({
selector: "my-app",
template: `
<div>
<p>Hello?</p>
<ng-template #helloThereMsg>
Hello There!
</ng-template>
</div>
<ng-template [ngTemplateOutlet]="realMsgVar"></ng-template>
`
})
export class AppComponent {
@ViewChild("helloThereMsg", { static: true }) realMsgVar;
}
Try this example on StackBlitz
Since the helloThereMsg template here lives directly in the host view, rather than being nested inside another view, rendering succeeds without the issues that came up with static: true. Adding an OnInit lifecycle hook would also let you access that same template reference.
ngOnInit() {
console.log(!!this.realMsgVar); // This would output true
}
You might be asking why you'd ever reach for static: false when ngOnInit already gives you access. The reasoning echoes the earlier point: with static: true, the ViewChild property stays frozen after the initial DoCheck lifecycle check, so reflecting a template from a child view never updates past undefined. That means once you set the testingMessageCompVar prop to true, the child component remains unrendered forever, because the value just stays undefined.
A concrete demonstration lives in the testingMessageCompVar example — flipping its value to true only locks in that undefined state.
Check out the live example on StackBlitz
View Limitations
Now that views are behind us, we must highlight a key constraint that governs them:
Properties of elements in a view can change dynamically, in response to user actions; the structure (number and order) of elements in a view can't. You can change the structure of elements by inserting, moving, or removing nested views within their view containers.
- Angular Docs
Embed Views
Although we've seen how ngTemplate can inject a component, Angular goes further: you can locate, track, mutate, or even construct views directly in your own component/directive code. 🤯
Here's a demonstration of rendering an ng-template from within TypeScript component logic:
@Component({
selector: 'my-app',
template: `
<ng-template #templ>
<ul>
<li>List Item 1</li>
<li>List Item 2</li>
</ul>
</ng-template>
<div #viewContainerRef class="testing">
</div>
`
})
export class AppComponent implements OnInit {
@ViewChild('viewContainerRef', {read: ViewContainerRef, static: true}) viewContainerRef;
@ViewChild('templ', {read: TemplateRef, static: true}) templ;
ngOnInit() {
this.viewContainerRef.createEmbeddedView(this.templ);
}
}
Check out this StackBlitz demo
This one packs quite a punch, so let's break it down piece by piece.
First, a quick refresher:
- Using the
ng-templatetag, we create a template that gets tied to the template reference variabletempl. - Additionally, a
divtag is set up and connected to the template reference variableviewContainerRef. - Finally,
ViewChildhands us a reference to the template through thetemplproperty on the component class.- Both can be labeled with
static: true, since neither one is shielded by parent non-host-view views.
- Both can be labeled with
Moving on to the fresh additions:
- We're also employing
ViewChildto link the template reference variableviewContainerRefto a property on the component class.- The
readprop is used to specify theViewContainerRefclass, which offers various methods for creating an embedded view.
- The
- In the
ngOnInitlifecycle hook, we call thecreateEmbeddedViewmethod from theViewContainerRefproperty to build an embedded view derived from the template.
Glance at the element debugger, and you'll spot the template placed as a sibling right next to the .testing div:
<!---->
<div class="testing"></div>
<ul>
<li>List Item 1</li>
<li>List Item 2</li>
</ul>
The empty comment
<!---->remains visible in the browser's element inspector and was intentionally retained. It serves as Angular's marker indicating where a template resides.
Although this has puzzled numerous developers who anticipated the embedded view to be a child of the ViewContainer reference element, the behavior is deliberate and aligns with other comparable APIs.
Under the hood, when a user requests a ViewContainer, Angular places it as the parent of the target element. The newly created view is then "inserted" into this container, since a container itself constitutes a view, and direct manipulation of a view's child count is impossible without spawning another view.
So, why designate a parent container rather than using the element directly?
Certain elements cannot accommodate child nodes—take </br> for instance. Consequently, the Angular team opted to assign the parent as the view container whenever a reference is requested through querying or dependency injection, as demonstrated in our case.
See How The View Is Tracked
Since views cannot alter their item count except through explicit movement, creation, or removal, the view container monitors all its views using an index system.
To inspect this index, one could employ a view container API that fetches the embedded view's position. This approach requires holding a reference to the embedded view within your template's logic.
Similar to ViewContainerRef, there exists EmbeddedViewRef. Fortunately, securing that reference is straightforward in our prior illustration, as it is what createEmbeddedView yields:
const embeddRef: EmbeddedViewRef<any> = this.viewContainerRef.createEmbeddedView(this.templ);
We can now call indexOf directly on the parent ViewContainerRef to locate the embedded view:
const embeddIndex = this.viewContainerRef.indexOf(embeddRef);
console.log(embeddIndex); // This would print `0`.
// Remember that this is a new view container made when we queried for one with DI, which is why this is the only view in it currently
The view container maintains a registry of every embedded view it manages, and a call to createEmbeddedView triggers a search for the precise insertion position within that registry. Using the get method, you can retrieve a specific embedded view by its index. For instance, to fetch all indexes tracked by viewContainerRef, you would write:
ngOnInit() {
for (let i = 0; i < this.viewContainerRef.length; i++) {
console.log(this.viewContainerRef.get(i));
}
}
Check out this example on StackBlitz
Context
In the same way that you can leverage contextRouterOutlet, createEmbeddedView allows you to supply context when you render a template. For instance, if you were building a counter component and needed a custom starting index, you could hand it a context, shaped exactly as we did earlier, like this:
import { Component, ViewContainerRef, OnInit, AfterViewInit, ContentChild, ViewChild, TemplateRef , EmbeddedViewRef} from '@angular/core';
@Component({
selector: 'my-app',
template: `
<ng-template #templ let-i>
<li>List Item {{i}}</li>
<li>List Item {{i + 1}}</li>
</ng-template>
<ul>
<div #viewContainerRef></div>
</ul>
`
})
export class AppComponent implements OnInit {
@ViewChild('viewContainerRef', {read: ViewContainerRef, static: true}) viewContainerRef;
@ViewChild('templ', {read: TemplateRef, static: true}) templ;
ngOnInit() {
const embeddRef3: EmbeddedViewRef<any> = this.viewContainerRef.createEmbeddedView(this.templ, {$implicit: 3});
const embeddRef1: EmbeddedViewRef<any> = this.viewContainerRef.createEmbeddedView(this.templ, {$implicit: 1});
}
}
Here, since our goal is to produce an unordered list whose items come from embedded views, we pull a ViewContainerRef straight off the unordered list itself.
But if you check your inspector — or simply scan the code — you'll spot an issue:
A stray div has appeared before your list items.
One workaround is the ng-container tag, which lets you grab a view reference without inserting any extra DOM element into the mix. Beyond that, ng-container is handy for grouping elements with no DOM wrapper, much like how React Fragments behave in that world.
<ng-container #viewContainerRef></ng-container>
See this example on StackBlitz
Move/Insert Template
Here's the catch — the sequence comes out wrong. The most direct fix, which likely springs to mind, is to swap the invocation order. Given that these are index-based, reversing the order of the two statements would neatly resolve the issue.
Yet this is a blog, and I deliberately cooked up a forced scenario to demonstrate programmatic view relocation:
const newViewIndex = 0;
this.viewContainerRef.move(embeddRef1, newViewIndex); // This will move this view to index 1, and shift every index greater than or equal to 0 up by 1
See this example on StackBlitz
A range of Angular APIs lets you take a view that already exists, relocate it, and alter it — all without generating a brand-new view or triggering change detection again.
Should you want to explore a different approach and find that createEmbeddedView operates at too high a level for your needs (we have to dig deeper), you can construct a view from a template and then embed it on your own.
ngOnInit() {
const viewRef1 = this.templ.createEmbeddedView({ $implicit: 1 });
this.viewContainerRef.insert(viewRef1);
const viewRef3 = this.templ.createEmbeddedView({ $implicit: 3 });
this.viewContainerRef.insert(viewRef3);
}
You can explore that implementation in the corresponding StackBlitz demo.
This behavior mirrors the actual source code, as shown in the internal logic of createEmbeddedView:
// Source code directly from Angular as of 8.0.1
createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number):
EmbeddedViewRef<C> {
const viewRef = templateRef.createEmbeddedView(context || <any>{});
this.insert(viewRef, index);
return viewRef;
}
Up to this point, we've relied on components alone when working with templates and altering their behavior. Yet, as we discussed earlier, the two are identical internally. This means that any template manipulation we've achieved with a component can equally be done with a directive. Here’s how that approach can play out:
@Directive({
selector: '[renderTheTemplate]'
})
export class RenderTheTemplateDirective implements OnInit {
constructor (private parentViewRef: ViewContainerRef) {
}
@ContentChild(TemplateRef, {static: true}) templ;
ngOnInit(): void {
this.parentViewRef.createEmbeddedView(this.templ);
}
}
@Component({
selector: 'my-app',
template: `
<div renderTheTemplate>
<ng-template>
<p>Hello</p>
</ng-template>
</div>
`
})
export class AppComponent {}
See this example on StackBlitz
This snippet bears a strong resemblance to the component code we've examined earlier.
Reference More Than View Containers
What sets this apart is the absence of a template bound to the directive, which unlocks intriguing possibilities. For instance, the same dependency injection method we've relied on to fetch the view container reference can also pull in a reference to the template element hosting the directive. This lets us render that template within the ngOnInit hook, as demonstrated here:
@Directive({
selector: '[renderTheTemplate]'
})
export class RenderTheTemplateDirective implements OnInit {
constructor (private parentViewRef: ViewContainerRef, private templToRender: TemplateRef<any>) {}
ngOnInit(): void {
this.parentViewRef.createEmbeddedView(this.templToRender);
}
}
@Component({
selector: 'my-app',
template: `
<ng-template renderTheTemplate>
<p>Hello</p>
</ng-template>
`
})
export class AppComponent {}
Check out this StackBlitz example
Input Shorthand
It’s possible to build an input that shares its name with the directive and then feed that input’s value straight into the template via a context:
@Directive({
selector: '[renderTheTemplate]'
})
export class RenderTheTemplateDirective implements OnInit {
constructor (private parentViewRef: ViewContainerRef, private templToRender: TemplateRef<any>) {}
@Input() renderTheTemplate: string;
ngOnInit(): void {
this.parentViewRef.createEmbeddedView(this.templToRender, {$implicit: this.renderTheTemplate});
}
}
@Component({
selector: 'my-app',
template: `
<ng-template [renderTheTemplate]="'Hi there!'" let-message>
<p>{{message}}</p>
</ng-template>
`
})
export class AppComponent {}
It’s worth emphasizing that this pattern is available across all directives. When the input shares its name with the directive, the value you supply is bound directly to that directive while the directive itself is attached to the component. This removes the need to specify the directive name and an input separately.
See this example on StackBlitz
This is starting to resemble ngTemplateOutlet quite closely, isn’t it? So why stop there? Let’s fully embrace that direction!
Using this approach, we can introduce a second input, provide an object as the context for the template slated for rendering, along with a template reference variable, and thereby mirror the API of Angular’s ngTemplateOutlet almost exactly:
@Directive({
selector: '[renderTheTemplate]'
})
export class RenderTheTemplateDirective implements OnInit {
constructor (private parentViewRef: ViewContainerRef) {
}
@Input() renderTheTemplate: TemplateRef<any>;
@Input() renderTheTemplateContext: Object;
ngOnInit(): void {
this.parentViewRef.createEmbeddedView(this.renderTheTemplate, this.renderTheTemplateContext);
}
}
@Component({
selector: 'my-app',
template: `
<ng-template [renderTheTemplate]="template1"
[renderTheTemplateContext]="{$implicit: 'Whoa 🤯'}"></ng-template>
<ng-template #template1 let-message>
<p>Testing from <code>template1</code>: <b>{{message}}</b></p>
</ng-template>
`
})
export class AppComponent {}
See this example on StackBlitz
The beauty here is twofold: from the outside, it mirrors the directive's syntax exactly, and Angular's internal implementation is quite close to this approach as well:
// This is Angular source code as of 8.0.1 with some lines removed (but none modified otherwise).
// The lines removed were some performance optimizations by comparing the previous view to the new one
@Directive({selector: '[ngTemplateOutlet]'})
export class NgTemplateOutlet implements OnChanges {
private _viewRef: EmbeddedViewRef<any>|null = null;
@Input() public ngTemplateOutletContext: Object|null = null;
@Input() public ngTemplateOutlet: TemplateRef<any>|null = null;
constructor(private _viewContainerRef: ViewContainerRef) {}
ngOnChanges(changes: SimpleChanges) {
if (this._viewRef) {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._viewRef));
}
if (this.ngTemplateOutlet) {
this._viewRef = this._viewContainerRef.createEmbeddedView(
this.ngTemplateOutlet, this.ngTemplateOutletContext);
}
}
}
In any Angular project, no matter how small, you'll encounter helper constructs that resemble directives but begin with a *, for example *ngIf or *ngFor. These constructs, called structural directives, rely on everything we've covered so far.
The core concept is that they are directives that place the element they're attached to inside a template, doing so without requiring an explicit ng-template element.
Here's a simple starting example:
@Directive({
selector: '[renderThis]'
})
export class RenderThisDirective implements OnInit {
constructor (private templ: TemplateRef<any>,
private parentViewRef: ViewContainerRef) {
}
ngOnInit(): void {
this.parentViewRef.createEmbeddedView(this.templ);
}
}
@Component({
selector: 'my-app',
template: `
<p *renderThis>
Rendering from <code>structural directive</code>
</p>
`
})
export class AppComponent {}
Try this example on StackBlitz
In the same manner that we previously tapped into Angular's DI (dependency injection) system to pull a ViewContainerRef, we now rely on DI to obtain the TemplateRef that the * in the directive's usage generates, and we embed a view from it.
All that computer science jargon a bit much? Same here—let's clarify. By placing the * at the front of the directive attached to an element, you're signalling Angular to wrap said element inside an ng-template and hand the directive over to that fresh template.
Once there, the constructor gives the directive access to that template (since Angular conveniently supplies it upon request—that's the DI system doing its job).
What's genuinely neat about structural directives, though? They're just directives at heart, so you can drop the * and apply them straight with an ng-template. Fancy using renderThis without a structural directive? Absolutely fine. Swap in the code snippet below, and your template renders as expected:
<ng-template renderThis>
<p>
Rendering from <code>ng-template</code>
</p>
</ng-template>
Try this example on StackBlitz
That's exactly why a single element can only carry one structural directive — otherwise, the framework would be at a loss about the wrapping order, let alone which template points to which reference.
Constructing a Simple *ngIf
However, a structural directive that does nothing more than render a template unchanged offers little value — take that directive off, and the behavior stays the same. But Angular ships with something fairly close to our original starting point: a handy tool for toggling views on a boolean's truthiness, called ngIf.
Thus, by adding an input that shares the directive's name (like we did in the previous step) which receives a value for truthiness checking, and inserting an if condition that only draws the view when the value is true, you've got the beginnings of our very own, hand-rolled ngIf stand-in!
@Directive({
selector: '[renderThisIf]'
})
export class RenderThisIfDirective implements OnInit {
constructor (private templ: TemplateRef<any>,
private parentViewRef: ViewContainerRef) {
}
@Input() renderThisIf: any; // `any` since we want to check truthiness, not just boolean `true` or `false`
ngOnInit(): void {
if (this.renderThisIf) {
this.parentViewRef.createEmbeddedView(this.templ);
}
}
}
@Component({
selector: 'my-app',
template: `
<label for="boolToggle">Toggle me!</label>
<input id="boolToggle" type="checkbox" [(ngModel)]="bool"/>
<div *renderThisIf="bool">
<p>Test</p>
</div>
`
})
export class AppComponent {
bool = false;
}
Check out this example on StackBlitz
That's quite neat, isn't it! Picture us expanding on this structural directive further, but when you run your tests (which you really should have 🙌), it becomes clear that flipping the checkbox doesn't produce any visible result. The reason lies in how it executes the check only at ngOnInit, not on subsequent input updates. So we need to fix that:
@Directive({
selector: '[renderThisIf]'
})
export class RenderThisIfDirective {
constructor (private templ: TemplateRef<any>,
private parentViewRef: ViewContainerRef) {
}
private _val: TemplateRef<any>;
@Input() set renderThisIf(val: TemplateRef<any>) {
this._val = val;
this.update();
}
update(): void {
if (this._val) {
this.parentViewRef.createEmbeddedView(this.templ);
}
}
}
See this example on StackBlitz
Notice that the OnInit lifecycle hook is gone, swapped out for an input setter. While we could have kept the lifecycle approach—switching to ngOnChanges to react to input changes, which works fine for a single input—this gets unwieldy when you have multiple inputs and need to persist local state, as the logic can quickly balloon in complexity.
When we run the tests again, a single toggle displays the embedded view; however, a second toggle fails to hide it. A straightforward tweak to the update method resolves the issue:
update(): void {
if (this._val) {
this.parentViewRef.createEmbeddedView(this.templ);
} else {
this.parentViewRef.clear();
}
}
See this example on StackBlitz
In this case, we call the clear method on the parent view ref to discard the prior view whenever the condition evaluates to false. Since our structural directive is tied exclusively to a single template, it's safe to assume that clear will only affect views generated by this directive, not ones originating elsewhere.
How Angular Built It
Angular's own approach is slightly more verbose, owing to extra capabilities included in its structural directive, yet the underlying logic aligns closely with what we've implemented.
The following is the Angular source code for that directive. For clarity within our current framework, several lines have been omitted, and one conditional expression has been adjusted just a bit. Otherwise, the code stays as it appears in the original.
@Directive({selector: '[ngIf]'})
export class NgIf {
private _context: NgIfContext = new NgIfContext();
private _thenTemplateRef: TemplateRef<NgIfContext>|null = null;
private _thenViewRef: EmbeddedViewRef<NgIfContext>|null = null;
constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>) {
this._thenTemplateRef = templateRef;
}
@Input()
set ngIf(condition: any) {
this._context.$implicit = this._context.ngIf = condition;
this._updateView();
}
private _updateView() {
if (this._context.$implicit) {
if (!this._thenViewRef) {
this._viewContainer.clear();
if (this._thenTemplateRef) {
this._thenViewRef =
this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context);
}
} else {
this._viewContainer.clear();
}
}
}
}
export class NgIfContext {
public $implicit: any = null;
public ngIf: any = null;
}
Let's take a moment to recap what each line is doing:
-
_contextinitializes with a default value of{$implicit: null, ngIf: null}- The structure of this object comes straight from the
NgIfContextclass shown below - It's there so the object can serve as a template context. In a simplified breakdown of the directive, this isn't strictly necessary, but it was kept to avoid having to patch other parts of the code
- The structure of this object comes straight from the
- Next, we declare a variable to hold both the template reference and the view reference (that's what
createEmbeddedViewgives back), so we can use them later - The constructor then sets the template reference into that variable and also obtains a handle on the view container
- We define an input that shares its name with a setter, much like in our own version
- This setter triggers an update function as well, just as ours did
- The view update checks whether the
$implicitvalue in the context is truthy — this works because we assign thengIfinput's value to the$implicitkey on the context - It then verifies if a view reference already exists
- If none exists, it proceeds to create one (after ensuring a template is available to build from)
- If one exists, it skips recreating it — this avoids the performance penalty of tearing down and rebuilding views repeatedly
Microsyntax
Okay, we've come a long way! The next part is going to be a bit of a challenge, so if you're running low on energy, it's totally okay to take a break and rest up. 😴 🛌 If not, let's stand up — do a quick shoulder roll to get the blood flowing 🏋 (I'm absolutely not just saying this so future me has a built-in excuse to pause during edits, definitely not 😬), and let's jump in.
Bind Context
Much like Angular takes the rest of your template and parses it to turn custom Angular components into standard template tags, Angular gives its own item query mechanism a miniature language of its own. The Angular devs call this a "microsyntax". It lets you build specialized APIs that tap into this syntax, invoking and using specific pieces of your logic. Hard to picture? Right there with you, what follows is a simple example:
function translatePigLatin(strr) {
// See the code here: https://www.freecodecamp.org/forum/t/freecodecamp-algorithm-challenge-guide-pig-latin/16039/7
}
@Directive({
selector: '[makePiglatin]'
})
export class MakePigLatinDirective {
constructor(private templ: TemplateRef<any>,
private parentViewRef: ViewContainerRef) {}
@Input() set makePiglatin(val: string) {
this.parentViewRef.createEmbeddedView(this.templ, {
$implicit: translatePigLatin(val)
});
}
}
@Component({
selector: 'my-app',
template: `
<p *makePiglatin="'This is a string'; let msg">
{{msg}}
</p>
`
})
export class AppComponent {}
See this example on StackBlitz
You've likely seen this pattern before. The $implicit value from the context is being accessed through our structural directive! But go back to the section where that concept was first introduced, and you'll spot that this syntax isn't identical to the template variable used for binding context from an ng-template tag, though it shares similarities.
In this case, the semicolon is what sets the two syntaxes apart. It terminates the preceding statement and initiates another one—the initial statement binds the makePiglatin property on the directive, while the subsequent one assigns the $implicit context value to the local template variable msg. This brief illustration already hints at one reason the microsyntax is so appealing—it enables a compact micro-language for defining your own APIs.
Now, let's delve into how this tool can offer further benefits. Suppose we aimed to expose multiple values in the context. How would those named values be bound?
@Directive({
selector: '[makePiglatin]'
})
export class MakePigLatinDirective {
constructor(private templ: TemplateRef<any>,
private parentViewRef: ViewContainerRef) {}
@Input() set makePiglatin(val: string) {
this.parentViewRef.createEmbeddedView(this.templ, {
$implicit: translatePigLatin(val),
original: val
});
}
}
@Component({
selector: 'my-app',
template: `
<p *makePiglatin="'This is a string'; let msg; let ogMsg = original">
The message "{{msg}}" is "{{ogMsg}}" in 🐷 Latin
</p>
`
})
export class AppComponent {}
See this example on StackBlitz
As was the case earlier, semicolons separate the definitions, after which the external context value (meaning, coming from the directive) named original gets assigned to the local template variable ogMsg.
Additional Attribute Inputs
Your standard — non-structural — directive exposes inputs you can place on it. Take a directive with these inputs, for example:
@Directive({
selector: '[consoleThing]'
})
export class ConsoleThingDirective {
@Input() set consoleThing(val: string) {
if (this.warn) {
console.warn(val)
return
}
console.log(val)
}
@Input() warn: boolean = false;
}
Next, invoke them using the template below:
<ng-template [consoleThing]="'This is a warning from the 👻 of code future, refactor this please'" [warn]="true"></ng-template>
See this example on StackBlitz
This approach proves highly valuable for keeping the public interface concise and making it easy to extend the directive with additional capabilities. Structural directives provide an analogous benefit, yet they rely on a distinct syntax and face certain constraints because of the microsyntax implementation.
@Directive({
selector: '[makePiglatin]'
})
export class MakePigLatinDirective implements OnInit {
constructor(private templ: TemplateRef<any>,
private parentViewRef: ViewContainerRef) { }
@Input() makePiglatin: string;
@Input() makePiglatinCasing: 'UPPER' | 'lower';
ngOnInit() {
let pigLatinVal = translatePigLatin(this.makePiglatin)
if (this.makePiglatinCasing === 'UPPER') {
pigLatinVal = pigLatinVal.toUpperCase();
} else if (this.makePiglatinCasing === 'lower') {
pigLatinVal = pigLatinVal.toLowerCase();
}
this.parentViewRef.createEmbeddedView(this.templ, {
$implicit: pigLatinVal,
original: this.makePiglatin
});
}
}
@Component({
selector: 'my-app',
template: `
<p *makePiglatin="'This is a string'; casing: 'UPPER'; let msg; let ogMsg = original">
The message "{{msg}}" is "{{ogMsg}}" in 🐷 Latin
</p>
`
})
export class AppComponent { }
Check out this StackBlitz example
It’s clear that I had to adjust our earlier pig latin directive example.
To get the timing right, I swapped out the setter for the input value and switched to ngOnInit instead.
Now I'm passing the value "upper" to makePiglatinCasing by writing casing: 'UPPER' in the structural directive's input and splitting it with a ;.
The real trick in the syntax lies with that input name. In earlier posts, I pointed out cases where similar names were just for clarity and not enforced by the syntax—this situation doesn’t fall into that category. What the microsyntax does is take the casing binding, capitalize the first letter, and stick that in front of the template selector to figure out which @Input directive property should receive the value.
That’s the reason we refer to the directive selector as the structural directive prefix—it needs to lead the names of all your microsyntax inputs. Aside from that prefix rule, there’s not much else to worry about with these input names. Fancy renaming it to makePiglatinCasingThingHere? Go ahead—just tweak the input syntax accordingly to casingThingHere: 'upper'
What’s wrong with binding like a regular input?
Back when I was first getting to grips with structural directives, I remember thinking, “this approach is neat, but it seems kind of unclear.” So I set out to tweak it a bit:
<p *makePiglatin="'This is a string'; let msg; let ogMsg = original" [makePiglatinCasing]="'UPPER'">
The message "{{msg}}" is "{{ogMsg}}" in 🐷 Latin
</p>
See this example on StackBlitz
Yet, instead of receiving applause for my pull request, I was met with a console error:
Can't bind to
makePiglatinCasingsince it isn't a known property ofp
At first, this might look puzzling, but here's the key: the structural directive encloses its host tag within a template. Consequently, the makePiglatinCasing input no longer targets the directive itself; it is instead assigned to the p element that resides inside the template generated by the structural directive.
This becomes clearer if you unwrap the syntax into its expanded form, like so:
<ng-template makePiglatin="'This is a string'; let msg; let ogMsg = original">
<p [makePiglatinCasing]="'UPPER'">
The message "{{msg}}" is "{{ogMsg}}" in 🐷 Latin
</p>
</ng-template>
Bind as you would — They're JUST directives!
Since structural directives are fundamentally no different from any other directive, you're free to apply the typical binding syntaxes you're already familiar with.
To take the broken example mentioned earlier and make it functional without relying on structural directives, here's how you'd rewrite it:
<ng-template [makePiglatin]="'This is a string'" [makePiglatinCasing]="'UPPER'" let-msg let-ogMsg="original">
<p>The message "{{msg}}" is "{{ogMsg}}" in 🐷 Latin</p>
</ng-template>
Open this example in StackBlitz
Using as to retain values in template variables
The as keyword stands out as a personal favorite within the microsyntax toolkit. At first glance, it appears quite logical and nearly identical to what let already offers:
It captures the context result of a given value and turns it into a template variable.
Given that explanation, it might seem redundant—and the truth is, it can serve in the exact same capacity:
<!-- These do exactly the same things -->
<p *makePiglatin="let msg casing 'UPPER'; original as ogMsg"></p>
<p *makePiglatin="let msg casing 'UPPER'; let ogMsg = original"></p>
Since makePiglatin exposes original, you can assign that value to a template variable labeled ogMsg.
Still, this case barely demonstrates the full strength of the as keyword: it lets you keep the very first value supplied to an input. That turns out to be especially handy with intricate expressions, like piped results (here, the uppercase pipe):
@Component({
selector: 'my-app',
template: `
<p *ngIf="message | uppercase as uppermessage">{{uppermessage}}</p>
<!-- Will output "HELLO THERE, WORLD" -->
`
})
export class AppComponent {
message = "Hello there, world"
}
Check out the live demo on StackBlitz
Although this scenario is easy to observe with this particular ngIf case, let's see what happens when we inject it into our existing pigLatin sample:
<p *makePiglatin="'test'; let msg; casing 'upper' | uppercase as upperInUpper">{{upperInUpper}}: {{msg}}</p>
See this example on StackBlitz
Here, 'upper' is meant to undergo transformation into 'UPPER' via the uppercase pipe, then flow into makePiglatinCasing as its input, with the resulting $implicit value being captured in a local variable named msg. Upon loading, the uppercased pig Latin renders correctly, but upperInUpper—which was expected to hold 'UPPER'—comes out as undefined.
This happens because no key from makePiglatinCasing is exported in our context to provide that value.
this.parentViewRef.createEmbeddedView(this.templ, {
$implicit: pigLatinVal,
original: this.makePiglatin,
makePiglatinCasing: this.makePiglatinCasing
});
See this example on StackBlitz
Once we pair the template variable with the as keyword during export, the expected value finally renders. But what’s happening under the hood? The short answer: as makes the bound output value public for template use. Here, the bound output is casing, since that’s the named target receiving the 'upper' input.
That capability opens the door to feeding arbitrary context into the template. For instance, you could modify the code as follows:
{
$implicit: pigLatinVal,
original: this.makePiglatin,
makePiglatinCasing: 'See? Any value'
}
Here's what the DOM would display:
Notice that: ISTHAY ISWAY AWAY ESTTAY — any value works.
Yet it worked with ngIf
That's accurate, but it holds because the Angular team deliberately designed the syntax to be intuitive, sparing users from needing to grasp its internals upfront.
If we look back at the section where ngIf source code was displayed, it's clear the same mechanism supplies the as value to ngIf:
this._context.$implicit = this._context.ngIf = condition;
Syntax Rules
Up to this point, I've aimed to keep the demonstration snippets in line with a somewhat uniform microsyntax. Consequently, you might assume that separating calls with ; is mandatory, that a specific ordering is required, or that additional hidden restrictions govern this syntax. That assumption would be wrong — the syntax is remarkably forgiving, though it can be tricky to grasp.
Parts Make Up The Whole
Den Approach the rules of microsyntax can seem daunting, so let's examine each component individually before assembling them.
Angular's microsyntax is built from 4 fundamental pieces, which when assembled in the right arrangement, form the full microsyntax API. These elements are:
- Expressions
- The
askeyword - Keyed expressions
-
letbindings
Expressions
To put it plainly, I define an expression as "anything that yields a value when evaluated". Taking the previous example, that could be applying an operator (5 + 3), invoking a function (Math.random()), referencing a variable (say const numberHere = 12, then numberHere), or even a literal ('a string here').
The question of "which pieces of JavaScript qualify as expressions" could fill an entire article on its own, but for now, a simple rule holds: if you can hand that snippet to a function as an argument, it counts as an expression.
<!-- This code is not super useful in the real-world, -->
<!-- but is used To demonstrate the correct syntaxes -->
<p *makePigLatin="'This is an expression'"></p>
<p *makePigLatin="'So is this' | uppercase"></p>
<p *makePigLatin="'So is ' + ' this'"></p>
<p *makePigLatin="varsToo"></p>
<p *makePigLatin="functionsAsWell()"></p>
The as keyword
The as keyword, when used in place of let, follows a simple set of rules.
- Your first step is to take the name of an exported key from the available context
- Next, you assign a new name to that value, which becomes the template input variable
For instance, with a context shaped like {personName: 'Corbin', personInterests: ['programming']}, extracting personInterests under the template input variable interestList looks like this: personInterests as interestList.
keyExp — Key Expressions
Key expressions are nothing more than expressions that you can feed into a structural directive’s input.
- The starting point is the
keythat you want to target, which corresponds to an input prefixed with the directive selector (for example,[ngIf]’sthenkey maps to thengIfTheninput) - Next, you may include a colon, and whether you do so has zero impact on the behavior
- Then, you supply the actual expression whose value gets passed into the input linked to the
keyyou picked - Last, if you need to capture that value, you can apply the
askeyword and give it a name to hold as a template input variable
<p *makePigLatin="inputKey: 'This is an expression' as localVar"></p>
<p *makePigLatin="inputKey: 'This is an expression'"></p>
<p *makePigLatin="inputKey 'This is an expression' as localVar"></p>
<p *makePigLatin="inputKey 'This is an expression'"></p>
let bindings
Here's how the let binding works:
- The reserved keyword
letcomes first - Next, you declare the template input variable that will hold the value
- Following the
=operator, you can specify which context key's value to capture- This part is not required. The optionality stems from the
$implicitkey present in the context. For instance, with a context like{$implicit: 1, namedKey: 900}, usinglet smallNum; let largerNum = namedKeyresults in1being stored insmallNumand900inlargerNum
- This part is not required. The optionality stems from the
Combining Them Together
With a clear grasp of each component in isolation, we can now assemble them for a comprehensive view of the microsyntax.
The
*reserved token kicks off any structural directive call — in this context, a token is simply a symbol designated for a specific action. Its role is to flag the directive call as structural for handling.Next comes the directive's
selectorvalue, which serves as a prefix for its inputs.The selector is bound just like a regular input, using the
="tokens.
The microsyntax itself lives inside the input’s content.
First Item
In the microsyntax, the first allowed entry is either an expression or a let binding.
When an expression like *prefix="5 + 3" is supplied, that value gets assigned to the input with the same name as the selector — for instance, the ngIf input on a directive that has [ngIf] as its selector value.
If a let binding comes first, it behaves exactly as described in the prior section
<!-- ✅ These ARE valid for the first item -->
<p *makePigLatin="'Expression'"></p>
<p *makePigLatin="let localVar = exportKey"></p>
<!-- 🛑 But these are NOT valid for the first item -->
<p *makePigLatin="inputKey: 'Input value expression'"></p>
<p *makePigLatin="exportKey as localVar"></p>
Second Item and Beyond
Once the first item is handled, you can supply either a let binding, an as binding, or a key expression. The microsyntax allows an unlimited number of these entries, provided each fits into one of those three categories. Their behavior matches what you’d anticipate from earlier examples. Still, an expression cannot be given to set the default input value — that capability is exclusive to the first item.
<p *makePigLatin="'First'; let localVar = exportKey"></p>
<p *makePigLatin="'First'; exportKey as localVar"></p>
<p *makePigLatin="'First'; inputKey: 'Input value expression'"></p>
<!-- And you can do more than one! -->
<p *makePigLatin="'First'; let localVar = exportKey; exportKey as localVar; inputKey: 'Input value expression'"></p>
Optional Separators
In the same way that the : can be omitted in a key expression, every separator within the microsyntax is also optional.
Each of the following examples is valid:
<!-- You can mix and match which tokens you leave or don't -->
<p *makePigLatin="'First'; let localVar = exportKey; exportKey as localVar; inputKey: 'Input value expression'"></p>
<!-- Remember that the key expression's `:` token is optional -->
<p *makePigLatin="'First'; let localVar = exportKey exportKey as localVar; inputKey 'Input value expression'"></p>
<!-- All separator tokens are optional -->
<p *makePigLatin="'First' let localVar = exportKey exportKey as localVar inputKey 'Input value expression'"></p>
<!-- You can shorten the `as` binding, as it's also part of the `let` binding -->
<!-- as an optional second part -->
<p *makePigLatin="'First' let localVar = exportKey as localVar; inputKey 'Input value expression'"></p>
Let's remake ngFor
The official docs on structural directives hint that digging into ngFor's implementation is a solid way to grasp the concept. We'll take that further and build our own from scratch.
To be fair, the real ngFor implementation is fairly involved and tackles much more than fits well here; so we'll craft a trimmed-down variant that covers only a slice of its features.
What features are we aiming for?
*uniFor="let item of items; let firstItem = isFirst"
That seems manageable. To keep things straightforward, we'll skip handling dynamic list updates and any teardown logic when the view is removed. These simplifications make the code easier to follow for this demo, but it wouldn't be safe for real-world use.
@Directive({ selector: '[uniFor]' })
export class UniForOf<T> implements AfterViewInit {
@Input() uniForOf: Array<T>;
constructor(
private viewContainer: ViewContainerRef,
private template: TemplateRef<any>
) {}
ngAfterViewInit() {
this.uniForOf.forEach((ofItem, i) => {
this.viewContainer.createEmbeddedView(this.template, {
isFirst: i === 0,
$implicit: ofItem,
uniForOf: this.uniForOf
})
})
}
}
@Component({
selector: 'my-app',
template: `
<p *uniFor="let num of numbers | async as allNumbers; let firstItem = isFirst">
Number in a list of {{allNumbers.length}} numbers: {{num}}
<ng-container *ngIf="firstItem"> it's the first number!</ng-container>
</p>
`
})
export class AppComponent {
// `import {of} from 'rxjs';`
numbers = of([1,2,3,4,5])
}
Check out the live demo on StackBlitz
- The first step is to enable
uniForas the name of the structural directive Next, we set up an input so that
ofcan be used as a key in the syntax, mirroring howngForworks.After that, we can access the value later through
this.uniForOf, exactly as shown in thengAfterViewInithook.-
Inside that lifecycle hook, we generate an embedded view for every element in the array
- Each view gets a context object with an implicit value, which allows
_varinlet _var of listto hold the current item - The context also includes an index, used to determine whether an item is the first in the collection
- Finally, we pass along a
uniForOfreference so theaskeyword can capture whatever is supplied afterofin the syntax
- Each view gets a context object with an implicit value, which allows
To wrap up, we rely on the async pipe to retrieve the array’s value from within an observable
Overall, Angular gives you a remarkably versatile set of built-in features for handling templates throughout your app. Even though many of the examples here were short, playful, and somewhat artificial, they were inspired by patterns I’ve encountered in substantial Angular libraries. So, adopting them can address a wide range of challenges and offer a solid foundation for building highly modular code.
And with that, we’re done — you've made it all the way through, congratulations! 🎊
I truly appreciate you sticking with this article. Feel free to ping me on Twitter or drop a comment below if you have questions, want to share insights, or just want to correct me—I’m always glad to assist and eager to pick up new knowledge!
