Angular structural directives and their microsyntax

Angular structural directives and their microsyntax

Have you ever wondered what's that star prefix for *ngIf and *ngFor? That's called a structural directive.

In this article, I'll show you what it is when you would want it and how it works.

I’ll also do a part 2, showing you how to create your own structural directives.

LinkTemplates are the structure

Let’s start defining what it is.

A structural directive is a directive with a structure. The structure is an ng-template. When you write <div><p>Text</p></div>, you’re telling Angular to “_declare the structure of a div tag, with a paragraph tag, with the string “Text”, and render it_”.

But when you wrap it in an <ng-template><div><p>Text</p></div></ng-template>, you’re telling Angular to “_declare the structure of a div tag, with a paragraph tag, with the string “Text”_”. But notice that now we’re not telling Angular to render it.

Now, put a directive in the <ng-template> and you have a structural directive: <ng-template [ngIf]=“condition”><div><p>Text</p></div></ng-template>

LinkSynthetic sugar

That's how ngIf works. Angular parses the <ng-template>, generating a TemplateRef, which is injected in the NgIf directive. If the condition passed to ngIf is true, the template is rendered.

But it would be very annoying to create an ng-template every time we wanted to use NgIf or any other directive that requires an ng-template. So the Angular team created synthetic sugar. Like a shortcut.

When you prefix your directive with a star, Angular wraps it in an ng-template and applies the directive to the ng-template. So <div *ngIf=“condition”>Abc</div>, becomes <ng-template [ngIf]=“condition”><div>Abc</div></ng-template>

It’s just synthetic sugar. You could write your whole app without the star prefix if you wanted.

LinkOnly one allowed

Knowing how it works, you can now understand why we can only use one structural directive per element. If you were to use *ngIf and *ngFor in the same element, how would Angular desugar that? ngIf first and then ngFor? The reverse? Both in the same template?

LinkMicrosyntax

Talking about ngFor, it seems much more complicated than ngIf, right? I've seen some really complex ngFor expressions, like passing a trackBy function, piping an observable array, grabbing the index, and checking if it’s the last element.

TypeScript
<div *ngFor="let item of list$ | async; trackBy: trackByFn; let itemIndex = index; let islast = last">{{ item }}</div>

Initially, I thought that was a ngFor-specific lingo, but it's not. It's a fully documented syntax that works for any structural directives, even ones that you end up creating. It's called the "structural directive microsyntax". (kinda obvious)

The structural directive microsyntax divides expressions by semicolons (;). In our NgFor example, we'd have 4 expressions:

  1. let item of list$ | async
  2. trackBy: trackByFn
  3. let itemIndex = index
  4. let islast = last

LinkDeclarations

Expressions starting with let are variable declarations. You declare the variable name right after let and use the equal sign (=) to point it to the name of the variable in the exported directive context.

That was a lot, sorry.

What I mean is that when we render an <ng-template>, we can optionally pass a context object. And the properties of this context object are passed to the template. The context object can have multiple explicit variables and a single implicit variable.

TypeScript
<!-- Rendering an <ng-template> with a context object -->
<ng-container *ngTemplateOutlet="templateExample; context: { $implicit: 'test', index: 1 }"></ng-container>

<!-- Using the context properties in the <ng-template> -->
<ng-template #templateExample let-itemIndex="index" let-item>
  <p>#{{ itemIndex }} - {{ item }}</p>
</ng-template>

It's like a JavaScript function, we have the parameters, which we declare and thus are very explicit, and we have this which is an implicit variable that exists even though we haven't declared it.

TypeScript
function example(itemIndex, isLast) {
  // Explicit
  console.log(itemIndex, isLast);

  // Implicit
  console.log(this);
}

In a function, you can have as many parameters as you want, but only one this. Just like that, in an ng-template, you can have as many explicit variables as you want, but only one implicit variable.

The implicit variable is what you get when you don't point to any exported variable. let item for example, is getting the implicit variable. But let isLast = last is getting the explicit last variable and let itemIndex = index is getting the explicit index variable.

After desugaring the variables, that's what we get:

TypeScript
<ng-template let-item let-itemIndex="index" let-isLast="last">
    <p>#{{ itemIndex }} - {{ item }}</p>
    <p *ngIf="isLast">The end</p>
</ng-template>

LinkKey expressions

Expressions with two arguments and an optional colon (:) between them are key expressions. The expression (in the right) gets assigned to the key (in the left) with a prefix before it.

Let's look at some examples.

In \*ngIf="condition; else otherTemplate, for the else otherTemplate expression:

  • ngIf is the prefix
  • else is the key
  • otherTemplate is the expression

That gets desugared to <ng-template [ngIfElse]="otherTemplate"></ng-template>

In *ngFor="let item of list; trackBy: trackByFn, for the trackBy: trackByFn expression:

  • ngFor is the prefix
  • trackBy is the key
  • trackByFn is the expression

That gets desugared to <ng-template [ngForTrackBy]="trackByFn"></ng-template>

Also, for that NgFor example, of list in let item of list is ALSO a key expression.

  • ngFor is the prefix
  • of is the key
  • list is the expression

That gets desugared to <ng-template [ngForOf]="list"></ng-template>

LinkLocal bindings

The last thing to mention is the optional as keyword at the end of the expression. It declares a template variable and maps the result of the expression to it.

*ngIf="condition as value" becomes <ng-template [ngIf]="condition" let-value="ngIf">

LinkConclusion

That's it. You now understand how structural directives work and how to analyze their microsyntax.

I'll do another article on how to code a custom structural directive from scratch and how to tell the Angular compiler to type-check its context.

Have a great day and I'll see you soon!

Join the newsletter and be the first to know when I launch a course, post a video or write an article.

I don't SPAM, one email per week tops.