Logo

dev-resources.site

for different kinds of informations.

<p *>Demystifying the Angular Structural Directives in a nutshell</p>

Published at
10/15/2023
Categories
angular
frontend
sharing
learning
Author
khangtrannn
Categories
4 categories in total
angular
open
frontend
open
sharing
open
learning
open
Author
11 person written this
khangtrannn
open
<p *>Demystifying the Angular Structural Directives in a nutshell</p>

1. Introduction

I'm sure you're definitely familiar with Angular's commonly used structural directives that we use all the time: ngIf, ngFor, and ngSwitch. But have you ever wondered about the โœจmagicโœจ behind that little asterisk (*)? In this post, I'll take you on a journey to demystify the power of Angular's micro syntax. Let's dive right in.

Let's begin by trying something NEW that you've probably never done before

๐Ÿ”ฅ Using *ngIf WITHOUT asterisk (*)

<div ngIf="true">Does it really work ๐Ÿค”?</div>
Enter fullscreen mode Exit fullscreen mode

If you try it on your own, you'll quickly discover that, unfortunately, it doesn't work, and the console will throw this error:

No provider for TemplateRef found

โ—Error: NG0201: No provider for TemplateRef found.

So what exactly is TemplateRef and why does removing the asterisk (*) from the ngIf directive trigger this error? (The answer awaits you in the upcoming section)

๐Ÿ”ฅ What happens if we use only the asterisk (*), as in the headline of this post?

<p *>Demystifying the Angular Structural Directives 
in a nutshell</p>
Enter fullscreen mode Exit fullscreen mode

Perhaps you'll experience the same sense of surprise I did when I first tried this: no errors, and nothing rendered in the view. It sounds like magic, doesn't it? So WHY does this happen??? ๐Ÿฃ

Everything happens for a reason.

Actually, the above syntax is just the shorthand (syntax desugaring) of <ng-template>. This convention is the shorthand that Angular interprets and converts into a longer form like the following:

<ng-template>
  <p>Demystifying the Angular Structural Directives 
in a nutshell</p>
<ng-template>
Enter fullscreen mode Exit fullscreen mode

And based on the Angular documentation, the <ng-template> is not rendered by default.

Angular <ng-template> documentation


*ngIf and *ngFor behave in a similar fashion. Angular automates the handling of <ng-template> behind the scenes for these directives as well.

// Shorthand
<div *ngIf="true">Just say hello</div>

// Long form
<ng-template [ngIf]="true">
  <div>Just say hello</div>
</ng-template>
Enter fullscreen mode Exit fullscreen mode
// Shorthand
<span *ngFor="let greenyPlant of ['๐ŸŒฑ', '๐ŸŒฟ', '๐Ÿ€']">
  {{greenyPlant}}
</span>

// Long form
<ng-template ngFor let-greenyPlant [ngForOf]="['๐ŸŒฑ', '๐ŸŒฟ', '๐Ÿ€']">
  <span>{{greenyPlant}}</span>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

You might be curious because, according to the documentation, <ng-template> isn't rendered by default, and we need to specifically instruct it to do so. So, what exactly does specifically instruct mean, and how do *ngIf and *ngFor handle the rendering of <ng-template>?


2. Create our own custom structural directive

Delve into Angular NgIf source code

Angular's NgIf directive source code

The first surprising detail, which might easily go unnoticed without a deeper look into the NgIf source code, is the absence of the asterisk (*) symbol in the NgIf selector. Instead, it ONLY uses plain '[ngIf]' as the selector. (the same applies to other Angular structural directives as well)

From the constructor, you'll find TemplateRef and ViewContainerRef. These are the two key elements required for rendering the template.

TemplateRef

First of all, we need the necessary information to render the template to the DOM, here is where TemplateRef comes into play. You can think of a TemplateRef as a blueprint for generating HTML template content.

<ng-template>
  <p>Everything placed inside ng-template can be
    referenced by using TemplateRef</p>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the internal workings of the TemplateRef.

@Directive({
  selector: '[greeting]',
  standalone: true,
})
export class GreetingDirective implements OnInit {
  #templateRef = inject(TemplateRef);

  ngOnInit(): void {
    console.log((this.#templateRef as any)._declarationTContainer.tView.template);
  }
}

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, GreetingDirective],
  template: `
    <p>Demystifying the Angular Structural Directives in a nutshell</p>
    <ng-template greeting>Xin chร o - Hello from Viet Nam ๐Ÿ‡ป๐Ÿ‡ณ!</ng-template>
  `,
})
export class App {}
Enter fullscreen mode Exit fullscreen mode

Under the hood, Angular will translate the <ng-template> into the TemplateRef's instructions, which we can later use to dynamically render the content of the <ng-template> into the view.

function App_ng_template_2_Template(rf, ctx) { if (rf & 1) {
    i0.ษตษตtext(0, "Xin ch\u00E0o - Hello from Viet Nam \uD83C\uDDFB\uD83C\uDDF3!");
} }
Enter fullscreen mode Exit fullscreen mode

ViewContainerRef

Until now, we know how Angular interprets TemplateRef's instructions, but how does Angular handle the process of hooking the template into the view?

Let's return to the previous example and inspect the DOM element.

<!DOCTYPE html>
<html class="ml-js">
  <head>...</head>
  <body>
    <my-app ng-version="16.2.8">
      <!--container-->
    </my-app>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

You'll notice that there is a special comment markup <!--container-->. This is exactly where the template will be inserted into the view.

Specifically, it is a comment node that acts as an anchor for a view container where one or more views can be attached. (view_container_ref.ts)

Angular ViewContainerRef source code

Let's make a minor update to the GreetingDirective to see how it works.

@Directive({
  selector: '[greeting]',
  standalone: true,
})
export class GreetingDirective implements OnInit {
  #vcr = inject(ViewContainerRef);

  ngOnInit(): void {
    console.log(this.#vcr.element.nativeElement);
  }
}
Enter fullscreen mode Exit fullscreen mode

You'll get exactly <!--container--> comment node from the console.

\<!--container--\> demo

So let's put everything together by rendering our <ng-template> into the view.

@Directive({
  selector: '[greeting]',
  standalone: true,
})
export class GreetingDirective implements OnInit {
  #templateRef = inject(TemplateRef);
  #vcr = inject(ViewContainerRef);

  ngOnInit(): void {
    this.#vcr.createEmbeddedView(#templateRef);
  }
}
Enter fullscreen mode Exit fullscreen mode

The final piece is createEmbeddedView. It essentially tells Angular to render the content defined in the TemplateRef and insert it within the element that the ViewContainerRef is associated with.

<ng-template> demo

Link to Stackbliz

Can we do it better?

You may wonder if there is a way to automatically render the <ng-template> instead of handling it manually so tedious. You're right; let's delegate the work to *ngTemplateOutlet.

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, NgTemplateOutlet],
  template: `
    <ng-container *ngTemplateOutlet="greetingTemplate"></ng-container>
    <ng-template #greetingTemplate>Xin chร o - Hello from Viet Nam ๐Ÿ‡ป๐Ÿ‡ณ!</ng-template>
  `,
})
export class App {}
Enter fullscreen mode Exit fullscreen mode

The following is a snippet from the source code of ng_template_outlet.ts. It handles exactly as the whole story we discovered above.

@Directive({
  selector: '[ngTemplateOutlet]',
  standalone: true,
})
export class NgTemplateOutlet<C = unknown> implements OnChanges {
  private _viewRef: EmbeddedViewRef<C>|null = null;

  ngOnChanges(changes: SimpleChanges) {
    ...

    if (this._shouldRecreateView(changes)) {
      // Create a context forward `Proxy` that will always bind to the user-specified context,
      // without having to destroy and re-create views whenever the context changes.
      const viewContext = this._createContextForwardProxy();
      this._viewRef = viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, viewContext, {
        injector: this.ngTemplateOutletInjector ?? undefined,
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's create a more complex directive using NASA Open APIs

NASA Planetary Directive Demo

We all know to render the TemplateRef within the view, we simply need to use the createEmbeddedView from ViewContainerRef. However, what if we want to pass the data from our custom directive to the TemplateRef? The answer is Template Context.

Template Context

Inside the <ng-template> tags you can reference variables present in the surrounding outer template. Additionally, a context object can be associated with <ng-template> elements. Such an object contains variables that can be accessed from within the template contents via template (let and as) declarations. ( context)

Understand template context with NgFor

I have no special talents. I am only passionately curious. - Albert Einstein

At the very beginning, when I first attempted to use NgFor, I just accepted the fact that we can use index, odd, even, and other data-binding context without wondering where they came from. It's a bit clearer to me now ๐Ÿฅฐ.

<div *ngFor="let greenyPlant of ['๐ŸŒฑ', '๐ŸŒฟ', '๐Ÿ€']; let i = index"
  [class.even]="even" [class.odd]="odd">
  {{greenyPlant}}
</div>
Enter fullscreen mode Exit fullscreen mode

In addition to the index, even, odd, you can reference all NgFor local variables and their explanation through NgFor documentation.

It's time to create our NASA Planetary Directive

We will create a custom structural directive to fetch the Astronomy Picture of the Day from NASA Open APIs and make the response data, including the title, image, and explanation, available through the template context for later use in our template.

interface NASAPlanetary {
  hdurl: string;
  title: string;
  explanation: string;
}

@Directive({
  selector: '[nasaPlanetary]',
  standalone: true,
})
export class NASAPlanetaryDirective implements OnInit {
  #templateRef = inject(TemplateRef);
  #vcr = inject(ViewContainerRef);
  #http = inject(HttpClient);

  ngOnInit(): void {
    this.#http.get<NASAPlanetary>('https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY')
      .pipe(take(1))
      .subscribe(({ title, hdurl, explanation }) => {
        this.#vcr.createEmbeddedView(this.#templateRef, {
          title, hdurl, explanation,
        });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

We can bind data to the template context by passing it as a second parameter of createEmbeddedView, as shown in the code above. You can find out more about it through the documentation.

Rendering Astronomy Picture of the Day into the view

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, HttpClientModule, NASAPlanetaryDirective],
  template: `
    <div *nasaPlanetary="
        let hdurl = hdurl; 
        let title = title; 
        explanation as explanation
      " 
      class="mt-5 d-flex justify-content-center">
      <div class="card" style="width: 18rem;">
      <img [src]="hdurl" class="card-img-top" [alt]="title">
      <div class="card-body">
        <h5 class="card-title">{{title}}</h5>
        <p class="card-text explaination">{{explanation}}</p>
      </div>
    </div>
  `,
  styles: [...],
})
export class App {}
Enter fullscreen mode Exit fullscreen mode

You can use either let or as to reference the data-binding context.

Indeed, the above code will be converted to <ng-template> long form as follows:

<ng-template nasPlanetary let-hdurl="hdurl" let-title="title" let-explanation="explanation">
  ...
</ng-template>
Enter fullscreen mode Exit fullscreen mode

Congratulations, you'll receive today's news from the S P A C E ๐Ÿš€๐ŸŒŒ.

Supporting Dynamic Date

But what if we want to retrieve a picture for a specific date rather than today? Let's enable our directive to support dynamic date through the use of the @Input decorator.

@Input('nasaPlanetary') date = new Date().toLocaleDateString('en-CA');
Enter fullscreen mode Exit fullscreen mode

With this input property, you can specify a date in the format YYYY-MM-DD to fetch an image from a particular day. If no date is provided, the directive will default to today.

To ensure that this input property works seamlessly with our directive, we need to make sure that the input alias matches the directive selector, which is nasaPlanetary in our case. So that it is recognized as the default input of the directive.

To fetch an image for a specific date, we'll need to modify the NASA Open APIs endpoint by including the date query parameter:

`https://api.nasa.gov/planetary/apod?date=${this.date}&api_key=DEMO_KEY`
Enter fullscreen mode Exit fullscreen mode

Finally, binding data to the directive can be achieved as follows:

// The data-binding context remains unchanged
<div *nasaPlanetary="'1998-01-08'; ..."></div>
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก Property bindding issue
Why can't we bind data to a directive input as we do with a normal component input?

<div *nasaPlanentary [date]="'1998-01-08'">
  Property bidding issue
</div>
Enter fullscreen mode Exit fullscreen mode

We'll end up with the following error:

Can't bind to 'date' since it isn't a known property of 'div'.
Enter fullscreen mode Exit fullscreen mode

Let's examine the long version of the code above to understand the underlying reason.

<div [date]="'1998-01-08'">
  <ng-template>Property bidding issue</ng-template>
</div>
Enter fullscreen mode Exit fullscreen mode

Indeed, the date property is applied to the <div> tag instead of <ng-template> itself. That's why Angular considers date property as a property of the <div> tag, rather than the input of the directive. And this is exactly the reason why we got the error Can't bind to 'date' since it isn't a known property of 'div'.

Supporting loading template

We can bind the default input of the directive by giving it the same name as the directive selector. But what if we want to add additional inputs to the directive? Let's achieve this by implementing support for a loading template.

Let's add a new input to support the loading template:

@Input('nasaPlanetaryLoading') loadingTemplate: TemplateRef<any> | null =
    null;
Enter fullscreen mode Exit fullscreen mode

If you notice, the above input follows the naming convention:

๐Ÿ’ก Directive input name = directiveSelector + identifier (first character capital)

For the identifier, we can choose whatever we want. In our case, since we want to add this input to support loading purposes, I've named it loading.

So let's define the loading template and add it to the directive:

<div *nasaPlanetary="'1998-01-08'; loading loadingTemplate; ...">

<ng-template #loadingTemplate>
  You can define whatever you want for the loading template.
  We will pass the loading template to the directive
  by using the #templateVariable.
</ng-template>
Enter fullscreen mode Exit fullscreen mode

Final demo

I believe that covers everything I've learned about Angular Structural Directives that I'd like to share in this post. While it may not be the most practical example for real-life projects, I hope you can still find something interesting.

Link to Stackbliz

NASA Planetary Structural Directive DEMO

3. Final thought

The most important thing in writing is to have written. I can always fix a bad page. I canโ€™t fix a blank one. - Nora Roberts

Thank you for making it to the end! This is my very first blog, and I'm thrilled to have completed it. I'm even more delighted if you found something helpful in my blog post. I'd greatly appreciate hearing your thoughts in the comments below, as it would be a significant source of motivation for me to create another one. โค๏ธ


Read More

Master the Art of Angular Content Projection


References

1. Angular documentation
2. Mastering Angular Structural Directives - The basics (Robin Goetz)
3. Unlocking the Power of ngTemplateOutlet - Angular Tiny Conf 2023 (Trung Vo)
4. Structural Directives in Angular โ€“ How to Create Custom Directive (Dmytro Mezhenskyi)

sharing Article's
30 articles in total
Favicon
Whatโ€™s your excuse for not using the web share API?
Favicon
Info card sharing service (free, no login required)
Favicon
Building a Tech Community from Scratch
Favicon
I Taught GIT to High School Students
Favicon
๐Ÿ˜ฎโ€๐Ÿ’จSaya Menyesal Beli Domain my.id
Favicon
<p *>Demystifying the Angular Structural Directives in a nutshell</p>
Favicon
Why I Like Writing Technical Blogs
Favicon
Add GitHub repository info, GitHub pages links and latest commits to any page using github-include
Favicon
Adding a "share to mastodon" link to any web site
Favicon
How do you differentiate Junior/Mid/Senior developer?
Favicon
Let anyone, anywhere, edit your static sites
Favicon
Digital Printing and Dev Printing: Are they the same?
Favicon
My โ€œWhat is Coding?โ€ Class
Favicon
Announcing Public Sharing: Share Your Drafts with the World on Contenda!
Favicon
sNationalDaysToday.com - Unite people to celebrate everyday
Favicon
Bugblogging
Favicon
The impact of sharing sessions in an engineering team
Favicon
Top 5 Things I Actually Like About Top {X} Posts
Favicon
Partnering with Google on web.dev
Favicon
Resource Helper website for easy access to daily usable links
Favicon
I make a list of free stuff/services for developers
Favicon
Sharing Local Server with Local Network (XAMPP)
Favicon
geek week local
Favicon
Adding RSS feed to my Nuxt & Storyblok blog
Favicon
What online image sharing service do you use?
Favicon
A Software to publish good code examples
Favicon
hackershare: Social bookmarking reinvented!
Favicon
Sharing keyboard & mouse across devices
Favicon
1 year experience, enjoying learning, but unfavorable atmosphere. Should I continue or not?
Favicon
Why I decided to start blogging and why we all should ๐Ÿ™Œ

Featured ones: