While playing with the concept of ng-template (html to to render), ng-container (work with ng-template for rendering) and ng-content (content projection), I'm having trouble to make use of $implicit value in a particular situation.
When the context is provided within *ngTemplateOutlet
using a ng-container and content projection,the implicit value is not present.
Cannot figure out why, any idea ?
See bellow the code :
<!-- app.component.html -->
<ng-template #inputTemplate let-labelDescription="inputLabel" let-defaultTagValue="inputValue">
<label for="action">{{labelDescription}}</label><input id="idAction" value="{{defaultTagValue}}">
</ng-template>
<ng-template #buttonTemplate let-labelDescription="buttonLabel" let-defaultTagValue="buttonValue">
<label for="action">label: {{labelDescription}}</label><h1><button id="idAction">my value : {{defaultTagValue}}</button></h1>
</ng-template>
<testing-componenet [id]="0" [description]="'send template button'">
<ng-container *ngTemplateOutlet="buttonTemplate; context: { buttonLabel:'LABEL_BUTTON', $implicit: '..data..'}"></ng-container>
</testing-componenet>
// testing-component.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'testing-component',
standalone: true,
imports: [],
template: "<p> {{id}} </p><p> {{description}} </p> <ng-content></ng-content>"
})
export class TestingComponentComponent {
@Input( {required:true} )
id!: number ;
@Input( {required:true} )
description!:string;
}
Result :
You have 2 options.
Since your second template expects a buttonLabel
and buttonValue
template variable
<!-- let-labelDescription="buttonLabel" let-defaultTagValue="buttonValue" -->
<ng-template #buttonTemplate let-labelDescription="buttonLabel" let-defaultTagValue="buttonValue">
<label for="action">label: {{labelDescription}}</label><h1><button id="idAction">my value : {{defaultTagValue}}</button></h1>
</ng-template>
You should use the following ngTemplateOutlet
snippet
<ng-container *ngTemplateOutlet="buttonTemplate; context: { buttonLabel:'LABEL_BUTTON', buttonValue: '..data..'}"></ng-container> <!-- not $implicit -->
On the other hand, you could use $implicit
if you wanted to
<ng-container *ngTemplateOutlet="buttonTemplate; context: { buttonLabel:'LABEL_BUTTON', $implicit: '..data..'}"></ng-container>
but then you have to pass in the following template
<ng-template #buttonTemplate let-labelDescription="buttonLabel" let-defaultTagValue> <!-- Remove ="buttonValue" -->
<label for="action">label: {{labelDescription}}</label><h1><button id="idAction">my value : {{defaultTagValue}}</button></h1>
</ng-template>
At the moment you're using <ng-content>
which projects the <ng-container>
and templateoutletdirective, and you're kinda lucky this works. A more slick way to do this is by writing a new directive
@Directive({ selector: 'testContent' })
export class TestContentDirective {
constructor(testComponent: TestingComponentComponent, template: TemplateRef<any>) {
testComponent.template = template;
}
}
So now you can use your component like this:
@Component({
selector: 'testing-component',
template: `
<p> {{id}} </p>
<p> {{description}} </p>
<ng-container *ngTemplateOutlet="template; context: { buttonLabel:'LABEL_BUTTON', buttonValue: '..data..'}"></ng-container>`,
})
export class TestingComponentComponent {
@Input({ required: true }) id!: number;
@Input({ required: true }) description!: string;
template: TemplateRef<any> | null = null;
}
@Directive({ selector: '[testContent]' })
export class TestContentDirective {
constructor(testComponent: TestingComponentComponent, template: TemplateRef<any>) {
testComponent.template = template;
}
}
@NgModule({
declarations: [TestingComponentComponent, TestContentDirective],
imports: [CommonModule],
exports: [TestingComponentComponent, TestContentDirective]
})
export class TestModule {}
@Component({
selector: 'app-root',
standalone: true,
imports: [TestModule],
template: `
<testing-component [id]="0" [description]="'send template button'">
<ng-template testContent let-labelDescription="buttonLabel" let-defaultTagValue="buttonValue">
<label for="action">label: {{labelDescription}}</label><h1><button id="idAction">my value : {{defaultTagValue}}</button></h1>
</ng-template>
</testing-component>
`,
})
export class App {
name = 'Angular';
}
bootstrapApplication(App);
To take things even further you can write it using the structural directive notation
<testing-component [id]="0" [description]="'send template button'">
<div *testContent="let labelDescription=buttonLabel; let defaultTagValue=buttonValue">
<label for="action">label: {{labelDescription}}</label><h1><button id="idAction">my value : {{defaultTagValue}}</button></h1>
</div>
</testing-component>
Or you could also use a default template using the coalescing operator:
<ng-container *ngTemplateOutlet="template ?? defaultTemplate; ..."></ng-container>
<ng-template #defaultTemplate let-labelDescription="buttonLabel" let-defaultTagValue="buttonValue">
<label for="action">label: Default description</label><h1><button id="idAction">my value: Default value</button></h1>
</ng-template>
So the conclusion is that you have 2 options. You can either use <ng-content select="[slot1]">
or you can use structural directives, assign the template to the main generic component, and project it while passing information from inside the generic component to wherever you're using it. I always tend to opt for the template approach, since it's more flexible and easier to pass data to the parent. I use it all the time. Here's an example component and here's how to use it.
If you would take a look now at where you use your structural directive, you'd see that the template variables you receive have the any
type
You can solve this by adding a ngTemplateContextGuard
static field to your directive (DOCS):
@Directive({ selector: '[testContent]' })
export class TestContentDirective {
constructor(testComponent: TestingComponentComponent, template: TemplateRef<any>) {
console.log('received a template');
testComponent.template = template;
}
public static ngTemplateContextGuard(
dir: TestContentDirective,
ctx: any
): ctx is BsTestTemplateContext {
return true;
}
}
export class BsTestTemplateContext {
// public $implicit: TData = null!;
buttonLabel!: string;
buttonValue!: string;
}
Result:
StackBlitz, but they don't show tooltips in templates.