Cody Pritchard
Cody Pritchard

Reputation: 897

Angular - Inject TemplateRef inherited from base component

I am trying to create some common UI components while using directive composition in Angular 19.

I have a base abstract directive that defines some initial classes and inputs:

@Directive({
  selector: '[abstractButton]',
})
export class AbstractButtonDirective {
  protected readonly _elementRef: ElementRef<HTMLButtonElement> = inject(ElementRef);

  readonly type = input<ButtonTypes>('button');
  readonly size = input<ButtonSizes>('sm');
  readonly color = input<ButtonColors | StateColors>();
  readonly shape = input<ButtonShapes>();
  ...

}

I have another abstract component that uses this as a host directive and defines a template:

@Component({
  selector: '[abstractActionButton]',
  imports: [CommonModule],
  template: `
    <ng-template
      #buttonTpl
      let-icon="icon"
      let-label="label"
    >
      <div class="flex items-center gap-2">
        <span>
          <i class="fa-lg fa-light {{ icon }}"></i>
        </span>
        <span
          [class.hidden]="responsive() || compact()"
          [class.md:block]="responsive() && !compact()"
        >
          {{ label }}
        </span>
      </div>
    </ng-template>
  `,
  hostDirectives: [{
    directive: AbstractButtonDirective,
    inputs: ['size', 'joined']
  }],
  changeDetection: ChangeDetectionStrategy.OnPush,
}) export class AbstractActionButtonComponent {
  private readonly _buttonTplRef = viewChild.required<TemplateRef<{icon: string, label: string}>>('buttonTpl');

  get buttonTpl() {
    return this._buttonTplRef();
  }

  readonly responsive = input(false, {
    transform: (value: boolean | string) =>
      typeof value === 'string' ? value === '' : value,
  });

  readonly compact = input(false, {
    transform: (value: boolean | string) =>
      typeof value === 'string' ? value === '' : value,
  });
}

Finally, I want to extend this component to create concrete action components, like a create, edit, delete, etc button, where the only thing that changes is button color, and icon being shown:

@Component({
  selector: '[createButton]',
  imports: [CommonModule],
  template: `
    <ng-container
      [ngTemplateOutlet]="buttonTpl"
      [ngTemplateOutletContext]="createBtnContext"
    />
  `,
  host: {
    '[class.btn-success]': 'true',
  },
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateButtonComponent extends AbstractActionButtonComponent {
  protected readonly createBtnContext = {
    icon: 'fa-circle-plus',
    label: 'Create'
  };
}

This would get used in my project like:

<button
   createButton
   [disabled]="createDisabled()"
   (click)="create.emit()"
>
   Create
</button>

The problem I am facing is the TemplateRef defined inside AbstractActionButtonComponent is undefined at the time I want to reference it in the CreateButtonComponent.

How can I ensure that this template is defined and available without having to explicitly define it in my other templates?

Im trying to maintain a clean DOM structure.

Upvotes: 1

Views: 32

Answers (1)

Naren Murali
Naren Murali

Reputation: 57986

When inherit an abstract component the decorator properties are not inherited so you need to explicitely define them in the using component's HTML, inheritance only applies to inheriting the class properties and methods.


Update you base component with @Directive decorator so that there is no metadata and it can be inherited.

@Directive()
export class AbstractActionButtonComponent {
  protected readonly _buttonTplRef: Signal<TemplateRef<any>> =
    viewChild.required<TemplateRef<any>>('buttonTpl');

  get buttonTpl() {
    return this._buttonTplRef();
  }

  readonly responsive = input(false, {
    transform: (value: boolean | string) =>
      typeof value === 'string' ? value === '' : value,
  });

  readonly compact = input(false, {
    transform: (value: boolean | string) =>
      typeof value === 'string' ? value === '' : value,
  });
}

Then use this component in your primary component, ensuring the decorators of your code are merged with the primary component decorator contents.

@Component({
  selector: '[createButton]',
  imports: [CommonModule],
  template: `
    <ng-template
        #buttonTpl
        let-icon="icon"
        let-label="label"
      >
        <div class="flex items-center gap-2">
          <span>
            <i class="fa-lg fa-light {{ icon }}"></i>
          </span>
          <span
            [class.hidden]="responsive() || compact()"
            [class.md:block]="responsive() && !compact()"
          >
            {{ label }}
          </span>
        </div>
      </ng-template>
    <ng-container
      [ngTemplateOutlet]="buttonTpl"
      [ngTemplateOutletContext]="createBtnContext"
    />
  `,
  hostDirectives: [
    {
      directive: AbstractButtonDirective,
      inputs: ['size', 'joined'],
    },
  ],
  host: {
    '[class.btn-success]': 'true',
  },
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateButtonComponent extends AbstractActionButtonComponent {
  protected readonly createBtnContext = {
    icon: 'fa-circle-plus',
    label: 'Create',
  };
}

Full Code:

import {
  Component,
  Directive,
  inject,
  ElementRef,
  input,
  ChangeDetectionStrategy,
  viewChild,
  TemplateRef,
  Signal,
} from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';

@Directive({
  selector: '[abstractButton]',
})
export class AbstractButtonDirective {
  protected readonly _elementRef: ElementRef<HTMLButtonElement> =
    inject(ElementRef);

  readonly type = input<any>('button');
  readonly size = input<any>('sm');
  readonly color = input<any>();
  readonly shape = input<any>();
  readonly joined = input<any>();
}

@Directive()
export class AbstractActionButtonComponent {
  protected readonly _buttonTplRef: Signal<TemplateRef<any>> =
    viewChild.required<TemplateRef<any>>('buttonTpl');

  get buttonTpl() {
    return this._buttonTplRef();
  }

  readonly responsive = input(false, {
    transform: (value: boolean | string) =>
      typeof value === 'string' ? value === '' : value,
  });

  readonly compact = input(false, {
    transform: (value: boolean | string) =>
      typeof value === 'string' ? value === '' : value,
  });
}

@Component({
  selector: '[createButton]',
  imports: [CommonModule],
  template: `
    <ng-template
        #buttonTpl
        let-icon="icon"
        let-label="label"
      >
        <div class="flex items-center gap-2">
          <span>
            <i class="fa-lg fa-light {{ icon }}"></i>
          </span>
          <span
            [class.hidden]="responsive() || compact()"
            [class.md:block]="responsive() && !compact()"
          >
            {{ label }}
          </span>
        </div>
      </ng-template>
    <ng-container
      [ngTemplateOutlet]="buttonTpl"
      [ngTemplateOutletContext]="createBtnContext"
    />
  `,
  hostDirectives: [
    {
      directive: AbstractButtonDirective,
      inputs: ['size', 'joined'],
    },
  ],
  host: {
    '[class.btn-success]': 'true',
  },
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateButtonComponent extends AbstractActionButtonComponent {
  protected readonly createBtnContext = {
    icon: 'fa-circle-plus',
    label: 'Create',
  };
}

@Component({
  selector: 'app-root',
  imports: [CreateButtonComponent],
  template: `
    <button
      createButton
      [disabled]="createDisabled()"
      (click)="click()"
    >
      Create
    </button>
  `,
})
export class App {
  click() {
    console.log('click');
  }

  createDisabled() {
    return true;
  }
}

bootstrapApplication(App);

Stackblitz Demo

Upvotes: 0

Related Questions