Home > Software design >  Angular global trackBy property directive with strict type checking
Angular global trackBy property directive with strict type checking

Time:03-02

I want make a unique trackBy directive by passing a property name for track the ngFor item, this is the code:

import { NgForOf } from '@angular/common';
import { Directive, Host, Input } from '@angular/core';

@Directive({
  selector: '[ngForTrackByProp]',
})
export class NgForTrackByPropDirective<T> {
  private propertyName: keyof T;

  constructor(@Host() public ngForOf: NgForOf<T>) {
    this.ngForOf.ngForTrackBy = this.trackBy.bind(this);
  }

  trackBy(index: number, item: T) {
    if (!this.propertyName) {
      throw new Error(`Property name not defined`);
    }
    if (typeof item[this.propertyName] === 'undefined') {
      throw new Error(`Property "${this.propertyName}" is undefined`);
    }
    const value = item[this.propertyName];
    console.log(
      `Item "${index}" trackByProp "${this.propertyName}" with value "${value}"`
    );
    return value;
  }

  @Input()
  set ngForTrackByProp(value: keyof T) {
    this.propertyName = value;
  }

  static ngTemplateContextGuard<T>(
    dir: NgForTrackByPropDirective<T>,
    ctx: any
  ): ctx is NgForTrackByPropDirective<T> {
    return true;
  }
}

Usage:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
  <ul *ngFor="let item of list; trackByProp: 'id'">
    <li>{{ item.id }} {{ item.name }}</li>
  </ul>`,
})
export class AppComponent {
  list = [
    { id: 0, name: 'foo' },
    { id: 1, name: 'bar' },
    { id: 2, name: 'baz' },
  ];
}

this the ONLINE DEMO

The code works, but I want to make sure the property passed is a key of the item of the collection and in case it isn't, let the error be a compile error, eg.:

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
  <ul *ngFor="let item of list; trackByProp: 'XXXXXXXXXXXXX'">
    <li>{{ item.id }} {{ item.name }}</li>
  </ul>`,
})
export class AppComponent {
  list = [
    { id: 0, name: 'foo' },
    { id: 1, name: 'bar' },
    { id: 2, name: 'baz' },
  ];
}

side note, my tsconfig.json:

"angularCompilerOptions": {
  "fullTemplateTypeCheck": true,
  "strictTemplates": true,
  "strictInjectionParameters": true
}

CodePudding user response:

You can narrow the type of an item in passing array by defining ngForOf @Input property:

import { NgIterable, ... } from '@angular/core';

...
export class NgForTrackByPropDirective<T> {

  @Input() ngForOf: NgIterable<T>;

  constructor(@Host() public ngForOfDir: NgForOf<T>) {
    this.ngForOfDir.ngForTrackBy = this.trackBy.bind(this);
  }
  ...

Forked Stackblitz

  • Related