W3cubDocs

/Angular

Structural directives

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 .

Structural directive shorthand

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.

One structural directive per element

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.

Creating a structural directive

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>
  1. 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.

  2. Import Input, TemplateRef, and ViewContainerRef.

    import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    
    @Directive({ selector: '[appUnless]'})
    export class UnlessDirective {
    }
  3. 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.

  4. 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.

    • If the condition is falsy and Angular hasn't created the view previously, the setter causes the view container to create the embedded view from the template
    • If the condition is truthy and the view is currently displayed, the setter clears the container, which disposes of the view

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;
    }
  }
}

Testing the directive

In this section, you'll update your application to test the UnlessDirective.

  1. Add a condition set to false in the AppComponent.

    condition = false;
  2. 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.

  3. 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.

Structural directive syntax reference

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

How Angular translates shorthand

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 prefix is added to the key
let
let-local="export"

Shorthand examples

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">

Improving template type checking for custom directives

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:

  • A property ngTemplateGuard_(someInputProperty) lets you specify a more accurate type for an input expression within the template
  • The ngTemplateContextGuard static property declares the type of the template context

This section provides examples of both kinds of type-guard property. For more information, see Template type checking.

Making in-template type requirements more specific with template guards

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.

Typing the directive's context

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>
Last reviewed on Mon Feb 28 2022

© 2010–2023 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://angular.io/guide/structural-directives