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?
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>
<div [appTableColumnCode]="'test'">
<ng-template appTableHeader let-data>
<!-- don't display column name in custom header for specific column -->
</ng-template>
</div>
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);