This guide is about structural directives and provides conceptual information on how such directives work, how Angular interprets their shorthand syntax, and how to add template guard properties to catch template type errors.
Structural directives are directives which change the DOM layout by adding and removing DOM elements.
Angular provides a set of built-in structural directives (such as NgIf
, NgForOf
, NgSwitch
and others) which are commonly used in all Angular projects. For more information see Built-in directives.
For the example application that this page describes, see the .
When structural directives are applied they generally are prefixed by an asterisk, *
, such as *ngIf
. This convention is shorthand that Angular interprets and converts into a longer form. Angular transforms the asterisk in front of a structural directive into an <ng-template>
that surrounds the host element and its descendants.
For example, let's take the following code which uses an *ngIf
to display the hero's name if hero
exists:
<div *ngIf="hero" class="name">{{hero.name}}</div>
Angular creates an <ng-template>
element and applies the *ngIf
directive onto it where it becomes a property binding in square brackets, [ngIf]
. The rest of the <div>
, including its class attribute, is then moved inside the <ng-template>
:
<ng-template [ngIf]="hero"> <div class="name">{{hero.name}}</div> </ng-template>
Note that Angular does not actually create a real <ng-template>
element, but instead only renders the <div>
element.
<div _ngcontent-c0>Mr. Nice</div>
The following example compares the shorthand use of the asterisk in *ngFor
with the longhand <ng-template>
form:
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd"> ({{i}}) {{hero.name}} </div> <ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById"> <div [class.odd]="odd"> ({{i}}) {{hero.name}} </div> </ng-template>
Here, everything related to the ngFor
structural directive is moved to the <ng-template>
. All other bindings and attributes on the element apply to the <div>
element within the <ng-template>
. Other modifiers on the host element, in addition to the ngFor
string, remain in place as the element moves inside the <ng-template>
. In this example, the [class.odd]="odd"
stays on the <div>
.
The let
keyword declares a template input variable that you can reference within the template. The input variables in this example are hero
, i
, and odd
. The parser translates let hero
, let i
, and let odd
into variables named let-hero
, let-i
, and let-odd
. The let-i
and let-odd
variables become let i=index
and let odd=odd
. Angular sets i
and odd
to the current value of the context's index
and odd
properties.
The parser applies PascalCase to all directives and prefixes them with the directive's attribute name, such as ngFor. For example, the ngFor
input properties, of
and trackBy
, map to ngForOf
and ngForTrackBy
.
As the NgFor
directive loops through the list, it sets and resets properties of its own context object. These properties can include, but aren't limited to, index
, odd
, and a special property named $implicit
.
Angular sets let-hero
to the value of the context's $implicit
property, which NgFor
has initialized with the hero for the current iteration.
For more information, see the NgFor API and NgForOf API documentation.
Note that Angular's
<ng-template>
element defines a template that doesn't render anything by default, if you just wrap elements in an<ng-template>
without applying a structural directive those elements will not be rendered.For more information, see the ng-template API documentation.
It's a quite common use-case to repeat a block of HTML but only when a particular condition is true. An intuitive way to do that is to put both an *ngFor
and an *ngIf
on the same element. However, since both *ngFor
and *ngIf
are structural directives, this would be treated as an error by the compiler. You may apply only one structural directive to an element.
The reason is simplicity. Structural directives can do complex things with the host element and its descendants.
When two directives lay claim to the same host element, which one should take precedence?
Which should go first, the NgIf
or the NgFor
? Can the NgIf
cancel the effect of the NgFor
? If so (and it seems like it should be so), how should Angular generalize the ability to cancel for other structural directives?
There are no easy answers to these questions. Prohibiting multiple structural directives makes them moot. There's an easy solution for this use case: put the *ngIf
on a container element that wraps the *ngFor
element. One or both elements can be an <ng-container>
so that no extra DOM elements are generated.
This section guides you through creating an UnlessDirective
and how to set condition
values. The UnlessDirective
does the opposite of NgIf
, and condition
values can be set to true
or false
. NgIf
displays the template content when the condition is true
. UnlessDirective
displays the content when the condition is false
.
Following is the UnlessDirective
selector, appUnless
, applied to the paragraph element. When condition
is false
, the browser displays the sentence.
<p *appUnless="condition">Show this sentence unless the condition is true.</p>
Using the Angular CLI, run the following command, where unless
is the name of the directive:
ng generate directive unless
Angular creates the directive class and specifies the CSS selector, appUnless
, that identifies the directive in a template.
Import Input
, TemplateRef
, and ViewContainerRef
.
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appUnless]'}) export class UnlessDirective { }
Inject TemplateRef
and ViewContainerRef
in the directive constructor as private variables.
constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) { }
The UnlessDirective
creates an embedded view from the Angular-generated <ng-template>
and inserts that view in a view container adjacent to the directive's original <p>
host element.
TemplateRef
helps you get to the <ng-template>
contents and ViewContainerRef
accesses the view container.
Add an appUnless
@Input()
property with a setter.
@Input() set appUnless(condition: boolean) { if (!condition && !this.hasView) { this.viewContainer.createEmbeddedView(this.templateRef); this.hasView = true; } else if (condition && this.hasView) { this.viewContainer.clear(); this.hasView = false; } }
Angular sets the appUnless
property whenever the value of the condition changes.
The complete directive is as follows:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; /** * Add the template content to the DOM unless the condition is true. */ @Directive({ selector: '[appUnless]'}) export class UnlessDirective { private hasView = false; constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) { } @Input() set appUnless(condition: boolean) { if (!condition && !this.hasView) { this.viewContainer.createEmbeddedView(this.templateRef); this.hasView = true; } else if (condition && this.hasView) { this.viewContainer.clear(); this.hasView = false; } } }
In this section, you'll update your application to test the UnlessDirective
.
Add a condition
set to false
in the AppComponent
.
condition = false;
Update the template to use the directive. Here, *appUnless
is on two <p>
tags with opposite condition
values, one true
and one false
.
<p *appUnless="condition" class="unless a"> (A) This paragraph is displayed because the condition is false. </p> <p *appUnless="!condition" class="unless b"> (B) Although the condition is true, this paragraph is displayed because appUnless is set to false. </p>
The asterisk is shorthand that marks appUnless
as a structural directive. When the condition
is falsy, the top (A) paragraph appears and the bottom (B) paragraph disappears. When the condition
is truthy, the top (A) paragraph disappears and the bottom (B) paragraph appears.
To change and display the value of condition
in the browser, add markup that displays the status and a button.
<p> The condition is currently <span [ngClass]="{ 'a': !condition, 'b': condition, 'unless': true }">{{condition}}</span>. <button type="button" (click)="condition = !condition" [ngClass] = "{ 'a': condition, 'b': !condition }" > Toggle condition to {{condition ? 'false' : 'true'}} </button> </p>
To verify that the directive works, click the button to change the value of condition
.
When you write your own structural directives, use the following syntax:
*:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )*"
The following tables describe each portion of the structural directive grammar:
as = :export "as" :local ";"?
keyExp = :key ":"? :expression ("as" :local)? ";"?
let = "let" :local "=" :export ";"?
Keyword | Details |
---|---|
prefix | HTML attribute key |
key | HTML attribute key |
local | Local variable name used in the template |
export | Value exported by the directive under a given name |
expression | Standard Angular expression |
Angular translates structural directive shorthand into the normal binding syntax as follows:
Shorthand | Translation |
---|---|
prefix and naked expression
| [prefix]="expression" |
keyExp |
[prefixKey] "expression" (let-prefixKey="export") NOTE: The |
let | let-local="export" |
The following table provides shorthand examples:
Shorthand | How Angular interprets the syntax |
---|---|
*ngFor="let item of [1,2,3]" | <ng-template ngFor let-item [ngForOf]="[1,2,3]"> |
*ngFor="let item of [1,2,3] as items; trackBy: myTrack; index as i" | <ng-template ngFor let-item [ngForOf]="[1,2,3]" let-items="ngForOf" [ngForTrackBy]="myTrack" let-i="index"> |
*ngIf="exp" | <ng-template [ngIf]="exp"> |
*ngIf="exp as value" | <ng-template [ngIf]="exp" let-value="ngIf"> |
You can improve template type checking for custom directives by adding template guard properties to your directive definition. These properties help the Angular template type checker find mistakes in the template at compile time, which can avoid runtime errors. These properties are as follows:
ngTemplateGuard_(someInputProperty)
lets you specify a more accurate type for an input expression within the templatengTemplateContextGuard
static property declares the type of the template contextThis section provides examples of both kinds of type-guard property. For more information, see Template type checking.
A structural directive in a template controls whether that template is rendered at run time, based on its input expression. To help the compiler catch template type errors, you should specify as closely as possible the required type of a directive's input expression when it occurs inside the template.
A type guard function narrows the expected type of an input expression to a subset of types that might be passed to the directive within the template at run time. You can provide such a function to help the type-checker infer the proper type for the expression at compile time.
For example, the NgIf
implementation uses type-narrowing to ensure that the template is only instantiated if the input expression to *ngIf
is truthy. To provide the specific type requirement, the NgIf
directive defines a static property ngTemplateGuard_ngIf: 'binding'
. The binding
value is a special case for a common kind of type-narrowing where the input expression is evaluated in order to satisfy the type requirement.
To provide a more specific type for an input expression to a directive within the template, add an ngTemplateGuard_xx
property to the directive, where the suffix to the static property name, xx
, is the @Input()
field name. The value of the property can be either a general type-narrowing function based on its return type, or the string "binding"
, as in the case of NgIf
.
For example, consider the following structural directive that takes the result of a template expression as an input:
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; import { Loaded, LoadingState } from './loading-state'; @Directive({ selector: '[appIfLoaded]' }) export class IfLoadedDirective<T> { private isViewCreated = false; @Input('appIfLoaded') set state(state: LoadingState<T>) { if (!this.isViewCreated && state.type === 'loaded') { this.viewContainerRef.createEmbeddedView(this.templateRef); this.isViewCreated = true; } else if (this.isViewCreated && state.type !== 'loaded') { this.viewContainerRef.clear(); this.isViewCreated = false; } } constructor( private readonly viewContainerRef: ViewContainerRef, private readonly templateRef: TemplateRef<unknown> ) {} static ngTemplateGuard_appIfLoaded<T>( dir: IfLoadedDirective<T>, state: LoadingState<T> ): state is Loaded<T> { return true; } }
export type Loaded<T> = { type: 'loaded', data: T }; export type Loading = { type: 'loading' }; export type LoadingState<T> = Loaded<T> | Loading;
import { Component } from '@angular/core'; import { LoadingState } from './loading-state'; import { Hero, heroes } from './hero'; @Component({ selector: 'app-hero', template: ` <button (click)="onLoadHero()">Load Hero</button> <p *appIfLoaded="heroLoadingState">{{ heroLoadingState.data | json }}</p> `, }) export class HeroComponent { heroLoadingState: LoadingState<Hero> = { type: 'loading' }; onLoadHero(): void { this.heroLoadingState = { type: 'loaded', data: heroes[0] }; } }
In this example, the LoadingState<T>
type permits either of two states, Loaded<T>
or Loading
. The expression used as the directive's state
input (aliased as appIfLoaded
) is of the umbrella type LoadingState
, as it's unknown what the loading state is at that point.
The IfLoadedDirective
definition declares the static field ngTemplateGuard_appIfLoaded
, which expresses the narrowing behavior. Within the AppComponent
template, the *appIfLoaded
structural directive should render this template only when state
is actually Loaded<Hero>
. The type guard lets the type checker infer that the acceptable type of state
within the template is a Loaded<T>
, and further infer that T
must be an instance of Hero
.
If your structural directive provides a context to the instantiated template, you can properly type it inside the template by providing a static ngTemplateContextGuard
function. The following snippet shows an example of such a function.
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appTrigonometry]' }) export class TrigonometryDirective { private isViewCreated = false; private readonly context = new TrigonometryContext(); @Input('appTrigonometry') set angle(angleInDegrees: number) { const angleInRadians = toRadians(angleInDegrees); this.context.sin = Math.sin(angleInRadians); this.context.cos = Math.cos(angleInRadians); this.context.tan = Math.tan(angleInRadians); if (!this.isViewCreated) { this.viewContainerRef.createEmbeddedView(this.templateRef, this.context); this.isViewCreated = true; } } constructor( private readonly viewContainerRef: ViewContainerRef, private readonly templateRef: TemplateRef<TrigonometryContext> ) {} // Make sure the template checker knows the type of the context with which the // template of this directive will be rendered static ngTemplateContextGuard( directive: TrigonometryDirective, context: unknown ): context is TrigonometryContext { return true; } } class TrigonometryContext { sin = 0; cos = 0; tan = 0; } function toRadians(degrees: number): number { return degrees * (Math.PI / 180); }
<ul *appTrigonometry="30; sin as s; cos as c; tan as t"> <li>sin(30°): {{ s }}</li> <li>cos(30°): {{ c }}</li> <li>tan(30°): {{ t }}</li> </ul>
© 2010–2023 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://angular.io/guide/structural-directives