angulartypescriptangular-directiveng-templateangular19

Double <ng-template> nesting with directive and contentChild = can't get second-level directive


Today I tried to make a custom table component. It must take column keys, default column template and column templates for some special columns to override their view. I use angular v19.2.

I have this table component:

@Component({
  selector: 'app-table',
  templateUrl: './table.component.html',
  styleUrls: ['./table.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
})
export class TableComponent<Item> {
  readonly items = input.required<Item[]>();

  readonly #columnService = inject(TABLE_COLUMN_SERVICE_TOKEN);

  readonly defaultHeaderTemplate = contentChild.required(TableHeaderDirective);
  readonly defaultCellTemplate = contentChild.required(TableCellDirective);
  readonly customColumnTemplates = contentChildren(TableColumnCodeDirective);

  readonly columnsConfig = this.#columnService.columnsConfig; //signal

  readonly columnsWithTemplates = signal<TableColumnConfigWithTemplates<Item>[]>([]);

  ngAfterContentChecked() {
    const columnConfigs = this.columnsConfig();
    const defaultHeaderTemplate = this.defaultHeaderTemplate();
    const defaultCellTemplate = this.defaultCellTemplate();
    const customColumnTemplates = this.customColumnTemplates();

    if (
      !columnConfigs ||
      !defaultHeaderTemplate ||
      !defaultCellTemplate ||
      !customColumnTemplates
    ) {
      this.columnsWithTemplates.set([]);
    }
    
    this.columnsWithTemplates.set(
      columnConfigs.map((columnConfig) => {
        const customColumnTemplate = customColumnTemplates.find(
          (customColumnTemplate) => customColumnTemplate.columnCode() === columnConfig.key,
        );

        // in this place i want to get a directive, but i getting undefined
        console.log(customColumnTemplate?.headerTemplate())
    
        return {
          ...columnConfig,
          headerTemplate:
            customColumnTemplate?.headerTemplate()?.templateRef ??
            defaultHeaderTemplate.templateRef,
          cellTemplate:
            customColumnTemplate?.cellTemplate()?.templateRef ?? defaultCellTemplate.templateRef,
        };
      }),
    );
  }
}

This directives:

@Directive({
  selector: '[appTableColumnCode]',
  standalone: true,
})
export class TableColumnCodeDirective {
  readonly columnCode = input.required({ alias: 'appTableColumnCode' });

  readonly headerTemplate = contentChild(TableHeaderDirective);
  readonly cellTemplate = contentChild(TableCellDirective);
}
@Directive({
  selector: '[appTableHeader]',
  standalone: true,
})
export class TableHeaderDirective {
  readonly data = input.required({ alias: 'appTableHeader' });

  readonly templateRef = inject(TemplateRef);
}
@Directive({
  selector: '[appTableCell]',
  standalone: true,
})
export class TableCellDirective {
  readonly data = input.required({ alias: 'appTableCell' });

  readonly templateRef = inject(TemplateRef);
}

And i want use them in template like this:

  <app-table [items]="items()">
    <ng-template appTableHeader let-data>
      {{ data }}
    </ng-template>

    <ng-template appTableCell let-data>
      {{ data }}
    </ng-template>

    <ng-template [appTableColumnCode]="columnKeys.actions">
      <ng-template appTableHeader let-data>
        <!-- don't display column name in custom header for specific column -->
      </ng-template>
    </ng-template>
  </app-table>

I need to get some specific first-level directive and then get second-level directives from her children objects in template.

I have already tried to findout something similar and tried to ask gpt, but he tells, that this variant is ok, but i still can get second-level directive. I've also tried descendants: true and varoius lifecycle hooks.

What am i doing wrong?


Solution

  • You cannot read a template inside a template, is my analysis. It could be because the template element, is virtual and not created in the DOM, unless you have an *ngTemplateOutlet.

    So we can either use an ng-container for the appTableColumnCode directive or use a div element then the inner directive is detected.

    When we use either of these, they are created in the DOM and hence the inner template is accessible.

    <ng-container [appTableColumnCode]="'test'">
      <ng-template appTableHeader let-data>
        <!-- don't display column name in custom header for specific column -->
      </ng-template>
    </ng-container>
    

    OR

    <div [appTableColumnCode]="'test'">
      <ng-template appTableHeader let-data>
        <!-- don't display column name in custom header for specific column -->
      </ng-template>
    </div>
    

    Full Code:

    import {
      Component,
      input,
      contentChild,
      contentChildren,
      inject,
      TemplateRef,
      Directive,
      ChangeDetectionStrategy,
      Injectable,
      signal,
    } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    
    @Injectable({ providedIn: 'root' })
    export class ColumnService {
      columnsConfig = signal([{ key: 'test' }]);
    }
    
    @Component({
      selector: 'app-table',
      templateUrl: './table.component.html',
      styleUrls: ['./table.component.scss'],
      changeDetection: ChangeDetectionStrategy.OnPush,
      standalone: true,
    })
    export class TableComponent<Item> {
      readonly items = input.required<Item[]>();
    
      readonly #columnService = inject(ColumnService);
    
      readonly defaultHeaderTemplate = contentChild.required(TableHeaderDirective);
      readonly defaultCellTemplate = contentChild.required(TableCellDirective);
      readonly customColumnTemplates = contentChildren(TableColumnCodeDirective);
    
      readonly columnsConfig = this.#columnService.columnsConfig; //signal
    
      readonly columnsWithTemplates = signal<any[]>([]);
    
      ngAfterContentChecked() {
        const columnConfigs = this.columnsConfig();
        const defaultHeaderTemplate = this.defaultHeaderTemplate();
        const defaultCellTemplate = this.defaultCellTemplate();
        const customColumnTemplates = this.customColumnTemplates();
    
        if (
          !columnConfigs ||
          !defaultHeaderTemplate ||
          !defaultCellTemplate ||
          !customColumnTemplates
        ) {
          this.columnsWithTemplates.set([]);
        }
    
        this.columnsWithTemplates.set(
          columnConfigs.map((columnConfig) => {
            const customColumnTemplate = customColumnTemplates.find(
              (customColumnTemplate) =>
                customColumnTemplate.columnCode() === columnConfig.key
            );
    
            // in this place i want to get a directive, but i getting undefined
            console.log(customColumnTemplate?.headerTemplate());
    
            return {
              ...columnConfig,
              headerTemplate:
                customColumnTemplate?.headerTemplate()?.templateRef ??
                defaultHeaderTemplate.templateRef,
              cellTemplate:
                customColumnTemplate?.cellTemplate()?.templateRef ??
                defaultCellTemplate.templateRef,
            };
          })
        );
      }
    }
    
    @Directive({
      selector: '[appTableColumnCode]',
      standalone: true,
    })
    export class TableColumnCodeDirective {
      readonly columnCode = input.required({ alias: 'appTableColumnCode' });
    
      readonly headerTemplate = contentChild(TableHeaderDirective);
      readonly cellTemplate = contentChild(TableCellDirective);
    
      ngAfterContentInit() {
        console.log(this.headerTemplate(), this.cellTemplate());
      }
    }
    @Directive({
      selector: '[appTableHeader]',
      standalone: true,
    })
    export class TableHeaderDirective {
      readonly data = input.required({ alias: 'appTableHeader' });
    
      readonly templateRef = inject(TemplateRef);
    }
    @Directive({
      selector: '[appTableCell]',
      standalone: true,
    })
    export class TableCellDirective {
      readonly data = input.required({ alias: 'appTableCell' });
    
      readonly templateRef = inject(TemplateRef);
    }
    
    @Component({
      selector: 'app-root',
      imports: [
        TableComponent,
        TableColumnCodeDirective,
        TableHeaderDirective,
        TableCellDirective,
      ],
      template: `
    <app-table [items]="items()">
        <ng-template appTableHeader let-data>
          {{ data }}
        </ng-template>
    
        <ng-template appTableCell let-data>
          {{ data }}
        </ng-template>  
    
        <ng-container [appTableColumnCode]="'test'">
          <ng-template appTableHeader let-data>
            <!-- don't display column name in custom header for specific column -->
          </ng-template>
        </ng-container>
      </app-table>
      `,
    })
    export class App {
      items = signal([]);
      name = 'Angular';
    }
    
    bootstrapApplication(App);
    

    Stackblitz Demo