Reputation: 897
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
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',
};
}
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);
Upvotes: 0