In my angular project, I an trying to enumerate the ng-template
elements defined in the HTML template, based on a Directive.
I am trying to follow the example given in the answer by Aleš Doganoc in the answer to the question:
Get multiple ng-template ref values using contentChildren in angular 5
I am using Angular 17.2, which is much newer than the version used in the original answer.
The directive is defined as:
import { Directive, TemplateRef, Input } from '@angular/core';
@Directive({
selector: '[tableColumn]'
})
export class TableColumnDirective
{
constructor(public readonly template: TemplateRef<any>) { }
@Input('tableColumn') columnName: string = '';
}
My app.component.html contains:
<my-table>
<ng-template tableColumn="firstname" let-firstname>
<h1>this template is for column firstName</h1>
</ng-template>
<ng-template tableColumn="lastName" let-lastname>
<h1>this template is for column lastName</h1>
</ng-template>
</my-table>
The component TestComponent has the selector my-table
.
It uses the code:
import { AfterContentInit, Component, ContentChildren, QueryList } from '@angular/core';
import { TableColumnDirective } from '../table-column.directive';
@Component({
standalone: true,
selector: 'my-table',
templateUrl: './test-component.html',
styleUrl: './test-component.scss'
})
export class TestComponent implements AfterContentInit {
@ContentChildren(TableColumnDirective) columnList!: QueryList<TableColumnDirective>;
ngAfterContentInit(){
console.log('column template list');
console.log(this.columnList.toArray());
}
}
This is, as far as I can tell, identical to the code proposed by Aleš Doganoc in his answer.
My expectation is, that in the method ngAfterContentInit
, the member this.columnList
should return the templates which have the directive tableColumn.
However, the collection is always empty.
The console output shows:
test-component.ts:15 column template list
test-component.ts:16 Array(0)
My test project is on GitHub at:
https://github.com/PhilJollans/content-children-demo
And available on Stackblitz at:
https://stackblitz.com/github.com/PhilJollans/content-children-demo
What am I doing wrong?
Background information:
I am trying to make a working version of the component ngx-property-grid, which is no longer being maintained.
This component uses this technique to enumerate templates.
I managed to achieve what you're looking for:
To achieve this, we need to edit some parts of the code.
Directive
I removed the constructor parameter since it was unnecessary and declared the directive as standalone
:
@Directive({
selector: '[tableColumn]',
standalone: true
})
export class TableColumnDirective {
constructor() { }
@Input('columnName') columnName = '';
}
Test.component.html
To render the HTML content placed inside the my-table
tag, Angular needs to be informed. This is done using ng-content
:
<h1>This is the temp component</h1>
<ng-content></ng-content>
I've added some references to the app.component.ts :
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TestComponent } from './test/test-component';
import { TableColumnDirective } from './table-column.directive';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
standalone: true,
imports: [
TestComponent,
CommonModule,
TableColumnDirective,
]
})
export class AppComponent { }
And finally, the actual HTML part:
<my-table>
<!-- Method A -->
<h1 tableColumn [columnName]="'firstName'">this template is for column firstName</h1>
<!-- Method B -->
<ng-template [ngIf]="true" tableColumn [columnName]="'lastName'">
<h1>this template is for column lastName</h1>
</ng-template>
<!-- Test A -->
<ng-template [ngIf]="true" tableColumn="test1" let-test1>
<h1>this template is for column test1</h1>
</ng-template>
<!-- Test B -->
<ng-template tableColumn="test2" let-test2>
<h1>this template is for column test2</h1>
</ng-template>
</my-table>
As you can see, I've provided four scenarios. The first two work as expected, while the other two don't. Let me explain why.
Method A
There isn't much to say honestly here. If you append a directive to an HTML tag, you can use the directive's property just like you would for @Input
.
Method B
This one is essentially your original code, with the addition of [ngIf]="true"
and passing the column name as a parameter. The reason the input parameter is the same as in the first case is because of that. The [ngIf]="true"
is necessary because Angular doesn't render the <ng-template>
by default. For this example, I've just set it to true, but there are better ways to achieve this, such as using ng-container
. You can find more details on ng-template
here.
Test A
This example kind of works and kind of doesn't. You can see the h1
being rendered, but you won't get the directive's reference since we're passing data in the wrong way. tableColumn="test1" let-test1
doesn't have any meaning in this context. So the ContentChildren
in the test.component.ts
understands that there is a DOM element, but it doesn't recognize it as the directive.
Test B
As I mentioned in Method B, ng-templates
aren't rendered by default, so you won't see it in the UI or in the ContentChildren
directive's scope. The reason you see it in the entry list - the last entry - is too complicated to explain here.
If you need more information, feel free to ask.