Airo Global Software

Think Beyond Future !

When we first start learning Angular, we learn that there are two types of directives: Attribute directives and structural directives. We will only look at Structural Directives in this section. This includes the ability to remove an element and replace it with something else, as well as the ability to create additional elements.

As you are aware, we must distinguish Structural directives from Attribute directives in code: Structural directives should be preceded by *: *ngFor, *ngIf. Actually, when I first read this, I thought the distinction was strange and even cumbersome. Let's see if we can figure out why we need this * for the structural directive.

We will implement three different structural directives throughout the article to help you grasp the main idea.

What is ng-template?

Before we go any further, let's make sure we're all on the same page and understand what ng-template is. Let's create a simple component using this element to see what Angular actually renders:

As you can see, we defined an ng-template with a span element inside the component template. However, we do not see this span in a browser. Doesn't it appear to be a waste of time? Wait a minute, of course, it's useful and serves a purpose.

What is ng-container?

Let's look at it again with a component creation:

We can see the content that we put inside the ng-container here, but the container itself is hidden. If you're familiar with React, you'll probably recognize the behaviour a fragment or abbreviation for it.

Connect ng-container and ng-template.

Actually, we can ask Angular to render content that we explicitly place inside of the ng-template. To do so, we must first complete the following steps: step 1: get a reference to ng-template in a component.

step 2: get a reference to a container (any DOM element) where we want to render ng-content. template's

Step 3: Render content in a container programmatically.

Step 1: we define a template reference #template for the ng-template element and gain access to it via the ViewChild decorator. (you can also ask for it using this: @Input('template', read: TemplateRef ) template: TemplateRefany>).

Step 2: In the template, define a container where we want to render a predefined template and get access to it in component:

We want to read it as ViewContainerRef. Keep in mind that we can use any DOM element for containers, not just ng-container, but it keeps our layout clean because Angular doesn't leave any ng-container feet in the layout.

Step 3: We'll only have access to the container and template during the ngAfterViewInit lifecycle, and we'll need to render a template in a container: We simply generated a view from a template and inserted it into the container.

Structural Directive

You may wonder why, rather than explaining structural directives first, I started with ng-template and ng-container. However, it is necessary to explain why we put * before these directives. And the answer is that when Angular sees *, it treats our template differently and adds the following elements: Angular encircles our template with the ng-template element. That is, if the ngFor directive was not implemented, we would see nothing. Angular also creates a placeholder space called embedded view, where the directive can decide what to insert inside of this empty view container, for example, inserting the content of ng-template in the specific time as we did above.

Example 1: Create your own ngIf directive. Assume that Angular does not have a built-in directive like ngIf and that we must create our own with the name customIf. Let's build it with the Angular CLI: ng g d directives/custom-if It automatically creates a custom-if.directive.ts file in the directives folder and declares it in AppModule:

@Directive({
  selector: '[appCustomIf]'
})
export class CustomIfDirective {
  constructor() { }
}

Because Angular does some work behind the scenes — wrapping our template in ng-template and creating a placeholder for any content — we can ask Angular to provide access to those elements in a function native code:

@Directive({
  selector: '[appCustomIf]'
})
export class CustomIfDirective {
  constructor(
     private template: TemplateRef,   
     private container: ViewContainerRef) { }
}

@Directive({ selector: '[appCustomIf]' }) export class CustomIfDirective { @Input() appCustomIf!: boolean; constructor( private template: TemplateRef,
private container: ViewContainerRef) { } }

If @Input is true, the final step is to render the template in a container in the ngOnInit method:

@Directive({
  selector: '[appCustomIf]'
})
export class CustomIfDirective implements OnInit {
  @Input() appCustomIf!: boolean;
  constructor(
     private template: TemplateRef,   
     private container: ViewContainerRef) { }
  ngOnInit() {
     if (this.appCustomIf) {   
          this.container.createEmbeddedView(this.templateRef);
      }
   }
}

Congratulations! You've carried out the first structural directive. However, I believe that implementing the custom ngFor directive would be more interesting. Let's give it a shot.

Example 2: Creating a custom ngFor directive.

Let us recall how ngFor is used:

< ul>
  < li *ngFor="let value of values; let index">
    {{index}} {{value}}
  < /li>
< /ul>

It may appear strange given that we know we can bind to directive only JS expressions that produce a single value. However, this one, which is used, generates multiple values, let the value of values. The first source of confusion may be that we attempt to map keywords with JS keywords that we use in conjunction with for...of, but it has nothing in common with this one. Angular has its own DSL language, but we can use any word we want. Let us proceed in chronological order.

First and foremost, the expression on the right side of directive ngFor is known as macro syntax. Let us try to describe its structure: In this case, context can be anything with which we want to render a template in the target container, but it must be set as an element along with values during iteration. Remember that we typically define a template type as TemplateRefany>, which is a type of context for our template.

Which name should we use to gain access to value in values is the more interesting part here. There are three parts:

  • The first part is the name of the directive (in our case, ngFor).
  • The second part is the name of the word before value (in our case, of).
  • The third part is the name of the word after value (in our case, of).

As I previously stated, you can use any word you want instead of, for example, iterate, and access to the value will be via @Input('ngForIterate'), which is also known as a binding key.

So far, so good, I hope. Let's get started with our customFor directive. As is customary, let's use Angular CLI to build scaffolding for a directive:

directives/customFor ng gd

@Directive({
  selector: '[appCustomFor]'
})
export class CustomForDirective {
  constructor() { }
}

|

To spice things up, let's define our microsyntax for the developing directive:

< ul *appCustomFor="let value iterate values; let index">
  < li>{{index}} {{value}}< /li>
< ul>

With the following decorator, we can gain access to values:

@Input('appCustomForIterate') items: any[]

We used the following API to render a template in a container:

this.containerRef.createEmbeddedView(this.templateRef)

and the method createEmbeddedView accepts the second argument, which is a template context:

this.containerRef.createEmbeddedView(this.templateRef, {
  '$implicit': '' // any value which we want
  index: 0 // any value which we want
})

Keep an eye out for the $implicit key, which in our case manipulates value for value in our expression. Let's take a look at how we might put this directive into action: import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appCustomFor]',
 })
 export class CustomForDirective implements OnInit {
  @Input('appCustomForIterate') items!: any[];
  constructor(
    private templateRef: TemplateRef<{'$implicit': any, index: number}>,
    private containerRef: ViewContainerRef
  ) {}
  ngOnInit() {
    for(let i = 0; i< this.items.length; i++){
    this.containerRef.createEmbeddedView(this.templateRef, {
        index: i,
        '$implicit': this.items[i]
      })
    }
  }
 }

Pay attention to the fact that I purposefully changed the key used in the built-in directive ngForinto iterate, so we can use this directive as follows:

< div *appCustomFor="let value iterate items; let i = index">
 
 {{value}} {{i}}
 < /div>

Example 3: Structural Directive-compliant custom carousel

Let's look at a more concrete example for production. Let us suppose we need to use Carousel. We must pass a list of images for the carousel to the directive, and the custom directive must display one current image with the option to move forward/backward.

< div *appCarousel="let image of images; let ctr = ctr">
 
 
 < img [src]="image" />
 
 < button (click)="ctr.prev()">Prev
 
 < button (click)="ctr.next()">Next
 
< /div>

Let's begin as usual by creating a directive with Angular CLI and injecting TemplateRef and ViewContainerRef into the function native code. Also, we need to get access to the value in the images variable, which we can do with a key binding @Input('appCarouselOf'):

import { Directive, Input, OnInit, ViewContainerRef, TemplateRef } from '@angular/core';
 
@Directive({
  selector: '[appCarousel]',
 })
 export class CarouselDirective {
  @Input('appCarouselOf') images!: string[];
 
 currentIndex = 0;
  
 constructor(
    private templateRef: TemplateRef,
    private viewContainer: ViewContainerRef
  ) {}
 }

So far, everything should be familiar. So, let's create a method that is in charge of template rendering in a container. Before we begin, keep in mind that the usage of this directive allows for the creation of images. In the template context, let ctr = ctr, we must pass two variables: $implicit maintains the current carousel image, ctr — controller in charge of image rotation

@Directive({
  selector: '[appCarousel]',
 })
 export class CarouselDirective implements OnInit {
  // skipped for brevity
  renderCurrentSlide(){
    this.viewContainer.clear();
   this.viewContainer.createEmbeddedView(this.templateRef, {
        ctr: this,
        '$implicit': this.images[this.currentIndex]
    })
  }
 }

And now we'll implement two methods that will be available in the controller: next and previous:

@Directive({
  selector: '[appCarousel]',
 })
 export class CarouselDirective implements OnInit {
  // skipped for brevity
  next(){
    this.currentIndex = this.currentIndex === this.images.length - 1 ? 0 : this.currentIndex + 1;
    this.renderCurrentSlide();
  }
  prev(){
    this.currentIndex = this.currentIndex - 1 < 0 ? this.images.length - 1: this.currentIndex - 1;
    this.renderCurrentSlide();
  }
 }

The full implementation:

import { Directive, Input, OnInit, ViewContainerRef, TemplateRef } from '@angular/core';
 @Directive({
  selector: '[appCarousel]',
})
export class CarouselDirective implements OnInit {
  @Input('appCarouselOf') images!: string[];
   currentIndex = 0;
   constructor(
    private templateRef: TemplateRef,
    private viewContainer: ViewContainerRef
  ) {}
   ngOnInit() {
    this.renderCurrentSlide();
  }
   renderCurrentSlide(){
    this.viewContainer.clear();
    this.viewContainer.createEmbeddedView(this.templateRef, {
        ctr: this,
        '$implicit': this.images[this.currentIndex]
    })
  }
 
  next(){
    this.currentIndex = this.currentIndex === this.images.length - 1 ? 0 : this.currentIndex + 1;
    this.renderCurrentSlide();
  }
   prev(){
    this.currentIndex = this.currentIndex - 1 < 0 ? this.images.length - 1: this.currentIndex - 1;
    this.renderCurrentSlide();
  }
}

If you have any doubt about Mastering Angular Structural Directives. Please Contact us through the given email. Airo Global Software will be your digital partner.

E-mail id: [email protected]

enter image description here Author - Johnson Augustine
Chief Technical Director and Programmer
Founder: Airo Global Software Inc
LinkedIn Profile: www.linkedin.com/in/johnsontaugustine/