angularng-templateng-contentng-container

Use of $implicit do not pass value when using content projection


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 :

enter image description here

Should be :
enter image description here


Solution

  • 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>
    

    Making it more awesome

    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.

    Adding a template context guard

    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

    Structural directive variables with 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:

    enter image description here

    StackBlitz, but they don't show tooltips in templates.