DEV Community

Lucas Paganini
Lucas Paganini

Posted on • Edited on • Originally published at lucaspaganini.com

Angular structural directives and their microsyntax

Angular structural directives and their microsyntax


See this and many other articles at lucaspaganini.com

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.

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

Syntactic 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 syntactic 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 syntactic sugar. You could write your whole app without the star prefix if you wanted.

Only 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?

Microsyntax

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.

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

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

Declarations

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.

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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.

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

  // Implicit
  console.log(this);
}
Enter fullscreen mode Exit fullscreen mode

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:

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

Key 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

Image description

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

Image description

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>

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

Image description

Image description

Conclusion

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 see you soon!

References

  1. Structural directives documentation Angular docs
  2. NgIf Directive implementation GitHub

Top comments (0)