angulartype-inferencengtemplateoutletangular-structural-directivetemplate-context

Structural directive - ngTemplateContextGuard not working


I've built a datatable compoonent where I'm using a structural directive to pass a rowTemplate:

<bs-datatable [data]="artists">
  <tr *bsRowTemplate="let artist">
    <td class="text-nowrap">{{ artist.name }}</td>
    <td class="text-nowrap">{{ artist.yearStarted }}</td>
    <td class="text-nowrap">{{ artist.yearQuit }}</td>
  </tr>
</bs-datatable>

I'm trying to provide type inference in my custom structural directive, so the generic type of the structural directive should be infered from the parent component. According to the documentation, you should be able to simply implement a ngTemplateContextGuard:

@Component({ ... })
export class DatatableComponent<TData> {
  @Input() data: TData[] = [];
}

@Directive({ selector: '[appDatatableRow]' })
export class DatatableRowDirective<TData> {
  constructor(public datatable: DatatableComponent<TData>, public template: TemplateRef<TData>) { }

  static ngTemplateContextGuard<TData>(dir: DatatableRowDirective<TData>, ctx: unknown)
    : ctx is DatatableRowContext<TData> {
      return true;
    }
}

The type inference on the component works as expected:

Angular component type inference

But I can't get the structural directive to infer this generic type:

Angular directive inside component type inference

The goal is obviously to have the $implicit variable inferred in VS Code:

ng-bootstrap datatable type-inference not working

Why is this not working as I would expect? What am I still missing? I want the structural directive to infer the generic type of the parent datatable.

Similar articles/discussions:


Solution

  • I ended up using the of notation of structural directives:

    @Directive({ selector: '[bsRowTemplate]' })
    export class BsRowTemplateDirective<TData> {
    
      constructor(private datatableComponent: BsDatatableComponent<TData>, templateRef: TemplateRef<BsRowTemplateContext<TData>>) {
        this.datatableComponent.rowTemplate = templateRef;
      }
    
      @Input() set bsRowTemplateOf(value: PaginationResponse<TData> | undefined) {
        this.datatableComponent.data = value;
      }
      
      public static ngTemplateContextGuard<TData>(
        dir: BsRowTemplateDirective<TData>,
        ctx: any
      ): ctx is BsRowTemplateContext<Exclude<TData, false | 0 | '' | null | undefined>> {
        return true;
      }
    }
    
    export class BsRowTemplateContext<TData = unknown> {
      public $implicit: TData = null!;
    }
    

    Datatable

    @Component({
      selector: 'bs-datatable',
      ...
    })
    export class BsDatatableComponent<TData> {
      ...
    
      rowTemplate?: TemplateRef<BsRowTemplateContext<TData>>;
      data?: PaginationResponse<TData>;
    }
    

    Usage

    <bs-datatable #tabel [(settings)]="settings" (settingsChange)="loadArtists()">
      <div *bsDatatableColumn="'Name'; sortable: true">
        1. Artist
      </div>
      <div *bsDatatableColumn="'YearStarted'; sortable: true">
        2. Year started
      </div>
      <div *bsDatatableColumn="'YearQuit'; sortable: true">
        3. Year quit
      </div>
    
      <tr *bsRowTemplate="let artist of artists">
        <td class="text-nowrap">{{ artist.name }}</td>
        <td class="text-nowrap">{{ artist.yearStarted }}</td>
        <td class="text-nowrap">{{ artist.yearQuit }}</td>
      </tr>
    </bs-datatable>