Angular Challenges: Third Edition

The purpose behind this Angular challenge series is to sharpen your abilities through hands-on, real-world scenarios. You have the option to submit your work via a pull request, which I or another reviewer can examine — just like in a professional setting or when contributing to open-source projects.

This third challenge focuses on unlocking the full capability of directives in Angular. Directives let you enhance template behavior seamlessly. By targeting specific HTML elements or attribute selectors, you can alter how your view behaves without cluttering your templates.

The CommonModule in Angular already provides a useful set of directives — NgIf, NgFor, NgTemplateOutlet, NgSwitch, etc. — which you likely rely on regularly in your work.

What may not be widely known is that every directive is customizable, whether it exists in your own codebase or comes from an external library.

In this exercise, we’ll be upgrading the ubiquitous NgFor directive.

If you haven't tackled this challenge yet, give it a shot by visiting Angular Challenges first, then return here to compare approaches. (PR submissions are welcome — I’ll review them.)


Let’s work through a classic situation for this challenge. We have a list of people that might be empty or undefined, and in that case we need to display a fallback message.

The conventional approach to this would be:

<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 start by verifying whether our list is empty or undefined. If it is, we present a message to the user; if not, we render the list items.

This solution is functional and highly legible. (Good readability is essential for maintainable code.) But wouldn't it be more elegant to offload that responsibility to NgFor, which is already handling our list?

We could accomplish something like this, which I believe is even clearer and removes a nesting level:

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

Nevertheless, this requires modifying code we don't own. How is that feasible? This is exactly where the strength of directives becomes apparent.

A directive is essentially an Angular class that listens for a specific attribute within the template. Multiple directives can target the same attribute to perform distinct operations or adjustments.

With this insight, we can craft a new directive using [ngFor] as its 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 accepts two inputs:

  • The collection to verify for emptiness or undefined state.
  • A reference to the empty-state Template displayed when the condition holds true.

We leverage the ngDoCheck lifecycle hook instead of ngOnChanges, as we aim to detect list changes even when the list itself undergoes mutation.

ngOnChanges fires only when the list reference changes (no mutation), whereas ngDoCheck runs on every change detection cycle. If you're confident that the list won’t be mutated by you or your team, ngOnChanges is the more performant option!

// immutable operation => trigger ngOnChange
list = [...list, item]

// mutable operation => doesn't trigger ngOnChange
list = list.push(item)

Within ngDoCheck, we first clear any existing empty-template view, then determine whether the criteria for showing the empty state are satisfied. If they aren't, the list items are rendered by NgFor as usual.

Limitation: The primary caveat is that both NgFor and NgForEmptyDirective must be included in the component's imports. Omitting either one silently breaks behavior, and the IDE won't flag it—unlike with components, the compiler can't verify whether a directive is required.

As of v14.2, this is less of an issue: the Angular language service now provides warnings to ensure that CommonModule directives are properly imported when used.

Angular language service directive

Angular v15 and beyond:

With v15, the Angular team introduced the hostDirective concept. It resembles inheritance; you can attach a directive to the host selector of your custom directive or component.

This removes the earlier inconvenience. Now, you only need to import our custom directive into the component's 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 name
export { NgForEmptyDirective as NgForEmpty };

Important: You must alter the selector and remap all ngFor host directive inputs. If you keep listening for ngFor and someone also imports NgFor or CommonModule, the list will render twice.

In our example, we map ngForOf to ngForEmptyOf. This is done using 'ngForOf:ngForEmptyOf'. To expose other inputs from NgFor (like trackBy), you'll need to include them in your inputs array as well.

Our component template can now be updated to:

@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 trust this challenge was both enjoyable and educational.

More challenges are available at Angular Challenges. Give them a try — I'd be glad to review your work!

Stay updated on future challenges by following me on Medium, Twitter, or Github.