Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
November 28, 2022 09:16 am GMT

NgFor Enhancement

Welcome to Angular challenges #3.

The aim of this series of Angular challenges is to increase your skills by practicing on real life exemples. Moreover you can submit your work though a PR which I or other can review; as you will do on real work project or if you want to contribute to Open Source Software.

The goal of this third challenge is to dive deep into the power of directive in Angular. Directive allows you to improve the behavior of your template. By listening to HTML tags, specific selectors, you can change the behavior of your view without adding complexity to your template.

Angular already comes with a set of handy directives (in CommonModule): NgIf, NgFor, NgTemplateOutlet, NgSwitch, that you certainly use in your daily life at work.

One hidden feature is that your can customize all directives to your liking; those that live in your project but also those that belong to third party libraries.

In this challenge, we will learn how to enhance the famous and widely used NgFor Directive.

If you havent done the challenge yet, I invite you to try it first by going to Angular Challenges and coming back afterward to compare your solution with mine. (You can also submit a PR that Ill review)

For this challenge, well be working on a very common example. We have a list of persons which can be empty or undefined and we want to display a message if this is the case.

The classic way of doing it will look like this:

<ng-container *ngIf="persons && persons.length > 0; else emptyList">  <div *ngFor="let person of persons">    {{ person.name }}  </div></ng-container><ng-template #emptyList>The list is empty !!</ng-template>

We first start by checking if our list is empty or undefined. If so, we display an empty message to the user, otherwise we display our list items.

This code works fine and its very readable. (One of the fundamental points for a maintainable code). But wouldnt it be nicer to simplify it and give this management to ngFor which already manages our list ?

We could write something like this, which I find even more readable and we removed one level of depth.

<div *ngFor="let person of persons; empty: emptyList">  {{ person.name }}</div><ng-template #emptyList>The list is empty !!</ng-template>

However we need to override a piece of code we dont own. How it that possible ? Its here where well encounter the power of directive.

Directive is just an Angular class looking for a specific attribute in our template. And multiple directives can look for the exact same attribut in order to apply different actions or modifications.

By knowing this, we can simply create a new directive and use [ngFor] as a selector.

@Directive({  selector: '[ngFor]', // same selector as NgForOf Directive of CommonModule  standalone: true,})export class NgForEmptyDirective<T> implements DoCheck {  private vcr = inject(ViewContainerRef);  // same input as ngFor, we just need it to check if list is empty  @Input() ngForOf?: T[] = undefined;  // reference of the empty template to display  @Input() ngForEmpty!: TemplateRef<unknown>;  // reference of the embeddedView of our empty template  private ref?: EmbeddedViewRef<unknown>;  // use ngDoChange if you are never mutating data in your app  ngDoCheck(): void {    this.ref?.destroy();    if (!this.ngForOf || this.ngForOf.length === 0) {      this.ref = this.vcr.createEmbeddedView(this.ngForEmpty);    }  }}

This directive takes two inputs:

  • The list of item to check if empty or undefined.
  • The reference to the empty Template to display if above condition is true.

We use the ngDoCheck life cycle instead of ngOnChange because we want to check if the list has changed even if we mutate the list.

ngOnChange will only be triggered if the list has NOT been mutated, however ngDoCheck will be executed at each change detection cycle. If you are 100% certain that you and your team wont mutate the list, ngOnChange is a better choice!

// immutable operation => trigger ngOnChangelist = [...list, item]// mutable operation => doesn't trigger ngOnChangelist = list.push(item)

Inside ngDoCheck, we first destroy the emptyTemplateView if it was created and we check if the conditions are made to display the emptyTemplateView otherwise the list items will be rendered by ngFor internal execution.

Drawback: The only drawback I see is we need to import NgFor and NgForEmptyDirective inside our component import array. If we forgot one of them, the component wont work as expected and we wont get any warning from our IDE since the compiler cannot know if a directive is needed or not, in comparison of a component.

Since v14.2, this is not completely true anymore: The language service is now warning us for all directives of CommonModule to be imported when used

Angular language service directive

Angular v15 and higher:

In v15, the Angular team introduce the concept of hostDirective. This is kind of similar to inheritance. We can apply a directive on the host selector of our custom directive or component.

Thanks to this we can get rid of our previous inconvenience. We can now only import our custom directive into our component import array.

// Enhance ngFor directive@Directive({  selector: '[ngForEmpty]',  standalone: true,  hostDirectives: [    // to avoid importing ngFor in component provider array    {      directive: NgFor,      // exposing inputs and remapping them      inputs: ['ngForOf:ngForEmptyOf'],    },  ],})class NgForEmptyDirective<T> implements DoChange {  private vcr = inject(ViewContainerRef);  // check if list is undefined or empty  @Input() ngForEmptyOf: T[] | undefined;  @Input() ngForEmptyElse!: TemplateRef<any>;  private ref?: EmbeddedViewRef<unknown>;  ngDoChange(): void {    this.ref?.destroy();    if (!this.ngForEmptyOf || this.ngForEmptyOf.length === 0) {      this.ref = this.vcr.createEmbeddedView(this.ngForEmptyElse);    }  }}// we export our directive with a smaller and nicer nameexport { NgForEmptyDirective as NgForEmpty };

Warning: We do need to change our selector name and remapped all ngFor hostDirective inputs. If we keep listening on ngFor selector and someone on your team add NgFor or CommonModule to the component array, the list will be render twice.

_In our exemple, we are remapping ngForOf to ngForEmptyOf. To Do so, we write ngForOf:ngForEmptyOf.

If you want to expose other inputs (like trackBy) from ngFor host directive, you must add them to your inputs array._

Our component template now can be rewritten:

@Component({  standalone: true,  imports: [NgForEmpty], // no need to import ngFor  selector: 'app-root',  template: `    <div *ngForEmpty="let person of persons; else: emptyList">      {{ person }}    </div>    <ng-template #emptyList>The list is empty !!</ng-template>    <button (click)="clear()">Clear</button>    <button (click)="add()">Add</button>  `,})export class AppComponent {  persons?: string[] = undefined;  clear() {    this.persons = [];  }  add() {    if (!this.persons) this.persons = [];    this.persons?.push('tutu');  }}

I hope you enjoyed this NgRx challenge and learned from it.

Other challenges are waiting for you at Angular Challenges. Come and try them. I'll be happy to review you!

Follow me on Medium, Twitter or Github to read more about upcoming Challenges!


Original Link: https://dev.to/this-is-angular/ngfor-enhancement-28o7

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To