Reader small image

You're reading from  Angular Cookbook - Second Edition

Product typeBook
Published inDec 2023
Reading LevelBeginner
PublisherPackt
ISBN-139781803233444
Edition2nd Edition
Languages
Tools
Right arrow
Author (1)
Muhammad Ahsan Ayaz
Muhammad Ahsan Ayaz
author image
Muhammad Ahsan Ayaz

Muhammad Ahsan Ayaz is a Google developers expert in Angular, a software architect, and a head instructor of JavaScript at the School of Applied Technology. He loves helping the start-up ecosystem and product owners to bring their ideas to life using JavaScript, Angular, and web technologies. He has built several open-source projects that he maintains and he speaks at events, along with creating articles and video courses.
Read more about Muhammad Ahsan Ayaz

Right arrow

Writing your first custom structural directive

In this recipe, you’ll write your first custom structural directive named showFor (or *appShowFor with the prefix). A structural directive is one that can add or remove elements from the DOM. So, with this directive, we will add the particular element to the DOM if a provided Boolean is true, and we will remove it after the specified time (provided as a number representing milliseconds).

Getting ready

The app that we are going to work with resides in start/apps/chapter02/ng-show-for-directive inside the cloned repository:

  1. Open the code repository in your code editor.
  2. Open the terminal, navigate to the code repository directory, and run the following command to serve the project:
    npm run serve ng-show-for-directive
    

    This should open the app in a new browser tab, and you should see the following:

    Figure 2.7: ng-show-for-directive app running on http://localhost:4200

How to do it…

  1. First of all, we’ll create a directive using the following command in the workspace root folder:
    cd start && nx g directive show-for --directory apps/chapter02/ng-show-for-directive/src/app/directives --standalone=false
    

    If asked, choose the @nx/angular:component schematics and choose the “As provided” action.

  1. Now, instead of the *ngIf directive in the app.component.html file on the element with the class "dialog", we can use our *appShowFor directive:
    ...
    <main class="content" role="main">
      <button (click)="toggleDialog()">Toggle Dialog</button>
      <div class="dialog" *appShowFor="showDialog">
        <div class="dialog__heading">...</div>
        <div class="dialog__body">...</div>
      </div>
    </main>
    
  2. Now that we have set the condition, we need to create two @Input properties inside the directive’s TypeScript file, one being a boolean property and one being a number. We’ll use a setter to intercept the Boolean value’s changes and will log the value to the console for now:
    import { Directive, Input } from '@angular/core';
    @Directive({
      selector: '[appShowFor]',
    })
    export class ShowForDirective {
      @Input() duration = 1500;
      @Input() set appShowFor(value: boolean) {
        console.log({ showForValue: value });
      }
    }
    
  3. If you tap on the Toggle Dialog button now, you should see the values being changed and reflected on the console, as follows:

Figure 2.8: Console logs displaying changes for the appShowFor directive values

  1. Now, we’re moving toward the actual implementation of showing and hiding the content based on the value being false and true respectively. For that, we first need the TemplateRef service and the ViewContainerRef service injected into the constructor of the if-not.directive.ts file. Let’s add these, as follows:
    import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    @Directive({
      selector: '[appShowFor]'
    })
    export class ShowForDirective{
      @Input() duration = 1500;
      @Input() set appShowFor(value: boolean) {
         console.log({ showForValue: value });
       }
       constructor(
         private templateRef: TemplateRef<any>,
         private viewContainerRef: ViewContainerRef
       ) {}
    }
    
  2. Now let’s show the element. We’re going to create a show method and we’ll call it when the value of the appShowFor property becomes true. The code should look as follows:
    ...
    export class ShowForDirective {
      @Input() duration = 1500;
      @Input() set appShowFor(value: boolean) {
        console.log({ showForValue: value });
        if (value) {
          this.show();
        }
      }
      show() {
        this.viewContainerRef.createEmbeddedView(
         this.templateRef
        );
      }
      constructor(...) {}
    }
    

    If you click the Toggle Dialog button now, you should be able to see the dialog as follows:

    Figure 2.9: Dialog being shown using the show method

  1. Let’s implement the logic of hiding the dialog. We’ll use an @Output() prop with an EventEmitter for this as we want the value of appShowFor that’s passed by the parent to be updated, instead of updating it within the directive. Modify the code as follows:
    import { ... , EventEmitter } from '@angular/core';
    ...
    export class ShowForDirective {
      @Input() duration = 1500;
      @Input() set appShowFor(value: boolean) {
        ...
      }
      @Output() elementHidden = new EventEmitter();
      show() {...}
      hide() {
        this.viewContainerRef.clear();
      }
      constructor(...) {}
    }
    
  2. Now that we have the hide method there, let’s call it after the duration time saved in the duration property of the directive. This is so the dialog hides after that duration. Modify the code of the show method as follows:
    show() {
      this.viewContainerRef.createEmbeddedView(
       this.templateRef
      );
      setTimeout(() => {
        this.elementHidden.emit();
      }, this.duration);
     }
    

    With this change, you’ll see that nothing happens if you click the Toggle Dialog button after the dialog is shown, i.e., it never gets hidden. For that, we need to listen to the elementHidden event emitter we just created.

  1. Let’s make the app.component.html listen to the elementHidden event listener to change the value of the showDialog property as follows:
    <div class="dialog" *appShowFor="showDialog"
      (elementHidden)="toggleDialog()">
        <div class="dialog__heading">
                I am a Dialog
        </div>
        <div class="dialog__body">
          And this is some random content
        </div>
      </div>
    

    With this change, you’ll notice that it still doesn’t work. Yep! Because we need to call the hide method when the value of showDialog passed as the appShowFor prop is set to false.

  1. Let’s call the hide method in the ShowForDirective (in the appShowFor property’s set method) when the value of appShowFor becomes false as follows:
    @Input() set appShowFor(value: boolean) {
        console.log({ showForValue: value });
        if (value) {
          this.show();
        } else {
          this.hide();
        }
      }
    

    The thing is… this still won’t work because a structural directive in Angular can’t emit values. Or even if it does, the parent element won’t be able to listen to it. The following Stack Overflow question discusses why and links to an open GitHub issue in the Angular repository as well: https://stackoverflow.com/q/44235638.

  1. To make our structural directive work, we need to get rid of the syntactic sugar it comes with. Let’s modify the app.component.html to use the directive in a different (expanded) way, as follows:
    <main class="content" role="main">
      <button (click)="toggleDialog()">Toggle Dialog</button>
      <ng-template [appShowFor]="showDialog"
        (elementHidden)="toggleDialog()">
        <div class="dialog">
          <div class="dialog__heading">
            I am a Dialog
          </div>
          <div class="dialog__body">
            And this is some random content
          </div>
        </div>
      </ng-template>
    </main>
    

    The dialog should be hidden now. Yay! But wait. Try clicking the Toggle Dialog button lots of times quickly. You’ll see that the app goes crazy. That’s because we end up having too many setTimeout functions registered.

  1. Let’s clear the setTimeout if we toggle the dialog to manually hide it. Update the code for the ShowForDirective class as follows:
    ...
    export class ShowForDirective {
      ...
      timer!: ReturnType<typeof setTimeout>;
      show() {
        this.viewContainerRef.createEmbeddedView(
          this.templateRef
        );
        this.timer = setTimeout(() => {
          this.elementHidden.emit();
        }, this.duration);
      }
      hide() {
        clearTimeout(this.timer);
        this.viewContainerRef.clear();
      }
      constructor(...) {}
    }
    

Awesome! You’ll notice that even if you click the Toggle Dialog button fast and too many times, the app behaves correctly.

How it works…

Structural directives in Angular are special for multiple reasons. First, they allow you to manipulate DOM elements—that is, not just showing and hiding but also adding and removing elements entirely from the DOM based on your needs. Moreover, they have the * prefix, which binds to all the magic Angular does behind the scenes. For example, Angular automatically provides the TemplateRef and ViewContainer for working with this directive. As an example, *ngIf and *ngFor are both structural directives that work behind the scenes with the <ng-template> directive containing the content you bind the directive to. They then create the required variables/properties for you in the scope of ng-template. In this recipe, we do the same. We use the TemplateRef service to access the <ng-template> directive that Angular creates for us behind the scenes, containing the host element to which our appShowFor directive is applied. We use the ViewContainerRef service to add the TemplateRef to the DOM via the createEmbeddedView method.

We do this when the value of the appShowFor property becomes true. Notice that we’re intercepting the property appShowFor using a setter. We learned about this in Chapter 1, Winning Components Communication. We then use a setTimeout to automatically notify the parent component that the value passed to the appShowFor property needs to be changed to false. We do this using an @Output() emitter named elementHidden. Notice that we’re not supposed to make it false within the directive. The parent component is supposed to do it and it will automatically reflect in the directive. Our directive is supposed to react to that change and hide (or remove) the TemplateRef from the ViewContainer. You can see that we do this in the hide method using the this.viewContainerRef.clear(); statement. One of the key things to learn from this recipe is that if we use syntactic sugar, i.e., *appShowFor, in the app.component.html, we can’t listen to the elementHidden event emitter. That’s because this is a quirk of Angular - there’s an open issue on GitHub about this (check the See also section). For this to work, we removed the syntactic sugar and expanded the syntax by using a <ng-template> to wrap our dialog’s HTML in step 11. Notice that we just used [appShowFor] to pass the showDialog variable instead of *appShowFor="showDialog". And we are also listening to the elementHidden event on the <ng-template> element itself.

See also

Previous PageNext Page
You have been reading a chapter from
Angular Cookbook - Second Edition
Published in: Dec 2023Publisher: PacktISBN-13: 9781803233444
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Author (1)

author image
Muhammad Ahsan Ayaz

Muhammad Ahsan Ayaz is a Google developers expert in Angular, a software architect, and a head instructor of JavaScript at the School of Applied Technology. He loves helping the start-up ecosystem and product owners to bring their ideas to life using JavaScript, Angular, and web technologies. He has built several open-source projects that he maintains and he speaks at events, along with creating articles and video courses.
Read more about Muhammad Ahsan Ayaz