Back to Angular

Structural directives

adev/src/content/guide/directives/structural-directives.md

22.2.0-next.013.2 KB
Original Source

Structural directives

Structural directives are directives applied to an <ng-template> element that conditionally or repeatedly render the content of that <ng-template>.

For everyday conditional and repeated rendering, use Angular's built-in control flow blocks (@if, @for, and @switch). Write a structural directive when you need reusable rendering behavior that control flow doesn't cover, such as gating content behind a permission check or providing a template with data from an external source.

Example use case

This guide uses a directive called SelectDirective as its running example. The directive fetches data from a given data source and renders its template when that data is available. It is named after the SQL keyword SELECT and matched with an attribute selector, [select].

SelectDirective has an input naming the data source to use, called selectFrom. The select prefix for this input is important for the shorthand syntax. The directive instantiates its <ng-template> with a template context providing the selected data.

Using this directive directly on an <ng-template> looks like this:

angular-html
<ng-template select let-data [selectFrom]="source">
  <p>The data is: {{ data }}</p>
</ng-template>

The structural directive can wait for the data to become available and then render its <ng-template>.

HELPFUL: Angular's <ng-template> element defines a template that doesn't render anything by default. If you wrap elements in an <ng-template> without applying a structural directive, those elements are not rendered.

For more information, see the ng-template API documentation.

Structural directive shorthand

Angular supports a shorthand syntax for structural directives which avoids the need to explicitly author an <ng-template> element.

Structural directives can be applied directly on an element by prefixing the directive attribute selector with an asterisk (*), such as *select. Angular transforms the asterisk in front of a structural directive into an <ng-template> that hosts the directive and surrounds the element and its descendants.

You can use this with SelectDirective as follows:

angular-html
<p *select="let data; from: source">The data is: {{ data }}</p>

This example shows the flexibility of structural directive shorthand syntax, which is sometimes called microsyntax.

When used in this way, only the structural directive and its bindings are applied to the <ng-template>. Any other attributes or bindings on the <p> tag are left alone. For example, these two forms are equivalent:

angular-html
<!-- Shorthand syntax: -->
<p class="data-view" *select="let data; from: source">The data is: {{ data }}</p>

<!-- Long-form syntax: -->
<ng-template select let-data [selectFrom]="source">
  <p class="data-view">The data is: {{ data }}</p>
</ng-template>

Shorthand syntax is expanded through a set of conventions. A more thorough grammar is defined below, but in the above example, this transformation can be explained as follows:

The first part of the *select expression is let data, which declares a template variable data. Since no assignment follows, the template variable is bound to the template context property $implicit.

The second piece of syntax is a key-expression pair, from source. from is a binding key and source is a regular template expression. Binding keys are mapped to properties by transforming them to PascalCase and prepending the structural directive selector. The from key is mapped to selectFrom, which is then bound to the expression source. This is why many structural directives will have inputs that are all prefixed with the structural directive's selector.

One structural directive per element

You can only apply one structural directive per element when using the shorthand syntax. This is because there is only one <ng-template> element onto which that directive gets unwrapped. Multiple directives would require multiple nested <ng-template>, and it's unclear which directive should be first. <ng-container> can be used to create wrapper layers when multiple structural directives need to be applied around the same physical DOM element or component, which allows the user to define the nested structure.

Creating a structural directive

A structural directive is a directive class that injects two dependencies:

  • TemplateRef gives the directive access to the content of the <ng-template> it is applied to.
  • ViewContainerRef represents the location in the DOM where the directive can render that template.

The directive controls rendering by creating, or not creating, embedded views from the template in the view container. The complete SelectDirective looks like this:

ts
import {Directive, TemplateRef, ViewContainerRef, inject, input} from '@angular/core';

export interface DataSource<T> {
  load(): Promise<T>;
}

@Directive({
  selector: '[select]',
})
export class SelectDirective {
  private templateRef = inject(TemplateRef);
  private viewContainerRef = inject(ViewContainerRef);

  selectFrom = input.required<DataSource<unknown>>();

  async ngOnInit() {
    const data = await this.selectFrom().load();
    this.viewContainerRef.createEmbeddedView(this.templateRef, {
      // Create the embedded view with a context object that contains
      // the data via the key `$implicit`.
      $implicit: data,
    });
  }
}

The selectFrom input names the data source the directive reads from. It uses input.required() because the directive can't do anything useful without a data source.

When Angular initializes the directive, it loads the data and then renders the template by calling createEmbeddedView(). The second argument is the template's context object: values the template can bind to with let declarations. Assigning the data to the $implicit key makes it the default value that let-data (or let data in shorthand) receives.

NOTE: This example renders its template once, when the directive initializes. It does not re-render when the bound data source changes.

Once the directive works, consider adding template type-checking support.

HELPFUL: The CLI command ng generate directive scaffolds a directive along with its test file.

Structural directive syntax reference

When you write your own structural directives, use the following syntax:

ts
_: prefix = "( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )_";

The following patterns describe each portion of the structural directive grammar:

ts
as = :export "as" :local ";"?
keyExp = :key ":"? :expression ("as" :local)? ";"?
let = "let" :local "=" :export ";"?
KeywordDetails
prefixHTML attribute key
keyHTML attribute key
localLocal variable name used in the template
exportValue exported by the directive under a given name
expressionStandard Angular expression

How Angular translates shorthand

Angular translates structural directive shorthand into the normal binding syntax as follows:

ShorthandTranslation
prefix and naked expression[prefix]="expression"
keyExp[prefixKey]="expression" (The prefix is added to the key)
let locallet-local="export"

Shorthand examples

The following table provides shorthand examples:

ShorthandHow Angular interprets the syntax
*myDir="let item of [1,2,3]"<ng-template myDir let-item [myDirOf]="[1, 2, 3]">
*myDir="let item of [1,2,3] as items; trackBy: myTrack; index as i"<ng-template myDir let-item [myDirOf]="[1,2,3]" let-items="myDirOf" [myDirTrackBy]="myTrack" let-i="index">
*ngComponentOutlet="componentClass"<ng-template [ngComponentOutlet]="componentClass">
*ngComponentOutlet="componentClass; inputs: myInputs"<ng-template [ngComponentOutlet]="componentClass" [ngComponentOutletInputs]="myInputs">
*myDir="exp as value"<ng-template [myDir]="exp" let-value="myDir">

Improving template type checking for custom directives

You can improve template type checking for custom directives by adding template guards to your directive definition. These guards help the Angular template type checker find mistakes in the template at compile time, which can avoid runtime errors. Two different types of guards are possible:

  • ngTemplateGuard_(input) lets you control how an input expression should be narrowed based on the type of a specific input.
  • ngTemplateContextGuard is used to determine the type of the context object for the template, based on the type of the directive itself.

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

Type narrowing with template guards

A structural directive in a template controls whether that template is rendered at run time. Some structural directives want to perform type narrowing based on the type of input expression.

There are two narrowings which are possible with input guards:

  • Narrowing the input expression based on a TypeScript type assertion function.
  • Narrowing the input expression based on its truthiness.

To narrow the input expression by defining a type assertion function:

ts
// This directive only renders its template if the actor is a user.
// You want to assert that within the template, the type of the `actor`
// expression is narrowed to `User`.
@Directive(...)
class ActorIsUser {
  actor = input<User | Robot>();

  static ngTemplateGuard_actor(dir: ActorIsUser, expr: User | Robot): expr is User {
    // The return statement is unnecessary in practice, but included to
    // prevent TypeScript errors.
    return true;
  }
}

Type-checking will behave within the template as if the ngTemplateGuard_actor has been asserted on the expression bound to the input.

Some directives only render their templates when an input is truthy. It's not possible to capture the full semantics of truthiness in a type assertion function, so instead a literal type of 'binding' can be used to signal to the template type-checker that the binding expression itself should be used as the guard:

ts
@Directive(...)
class CustomIf {
  condition = input.required<boolean>();

  static ngTemplateGuard_condition: 'binding';
}

The template type-checker will behave as if the expression bound to condition was asserted to be truthy within the template.

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 type assertion function. This function can use the type of the directive to derive the type of the context, which is useful when the type of the directive is generic.

For the SelectDirective described above, you can implement an ngTemplateContextGuard to correctly specify the data type, even if the data source is generic.

ts
// Declare an interface for the template context:
export interface SelectTemplateContext<T> {
  $implicit: T;
}

@Directive(...)
export class SelectDirective<T> {
  // The directive's generic type `T` will be inferred from the `DataSource` type
  // passed to the input.
  selectFrom = input.required<DataSource<T>>();

  // Narrow the type of the context using the generic type of the directive.
  static ngTemplateContextGuard<T>(dir: SelectDirective<T>, ctx: any): ctx is SelectTemplateContext<T> {
    // As before the guard body is not used at runtime, and included only to avoid
    // TypeScript errors.
    return true;
  }
}

What's next

<docs-pill-row> <docs-pill href="guide/directives/directive-composition-api" title="Directive composition API"/> <docs-pill href="guide/templates/ng-template" title="ng-template"/> <docs-pill href="guide/templates/control-flow" title="Control flow"/> </docs-pill-row>