Production attribute directives go beyond simple @HostBinding and @HostListener. Injecting services, exporting directive instances for template access, using input aliases, and composing multiple directives enable powerful, reusable DOM behaviour abstractions. A PermissionsDirective that shows or hides elements based on the authenticated user’s roles eliminates role-checking boilerplate from every component that needs permission-gated UI elements.
Advanced Directive Patterns
// ── PermissionsDirective — show/hide based on user role ──────────────────
// Usage: <button *appHasRole="'Admin'">Delete All</button>
// Also: <div *appHasRole="['Admin', 'Moderator']">...</div>
@Directive({
selector: '[appHasRole]',
standalone: true,
})
export class HasRoleDirective implements OnInit, OnDestroy {
private authService = inject(AuthService);
private template = inject(TemplateRef<any>);
private viewContainer = inject(ViewContainerRef);
private destroyRef = inject(DestroyRef);
@Input({ required: true, alias: 'appHasRole' })
roles!: string | string[];
ngOnInit(): void {
this.authService.currentUser$.pipe(
takeUntilDestroyed(this.destroyRef),
).subscribe(user => {
const requiredRoles = Array.isArray(this.roles) ? this.roles : [this.roles];
const hasRole = user !== null &&
requiredRoles.some(role => user.roles.includes(role));
this.viewContainer.clear();
if (hasRole) {
this.viewContainer.createEmbeddedView(this.template);
}
});
}
}
// ── Exportable directive — template can call its methods ──────────────────
// Usage: <input appAutoResize #resizer="appAutoResize">
// <button (click)="resizer.reset()">Reset</button>
@Directive({
selector: 'textarea[appAutoResize]',
standalone: true,
exportAs: 'appAutoResize', // makes #ref="appAutoResize" work in templates
host: {
'(input)': 'onInput()',
'[style.overflow]': '"hidden"',
'[style.resize]': '"none"',
},
})
export class AutoResizeDirective implements AfterViewInit {
private el = inject(ElementRef<HTMLTextAreaElement>);
private renderer = inject(Renderer2);
ngAfterViewInit(): void { this.resize(); }
onInput(): void { this.resize(); }
private resize(): void {
const el = this.el.nativeElement;
this.renderer.setStyle(el, 'height', 'auto');
this.renderer.setStyle(el, 'height', el.scrollHeight + 'px');
}
// Public method accessible via #ref="appAutoResize"
reset(): void {
this.renderer.setProperty(this.el.nativeElement, 'value', '');
this.renderer.setStyle(this.el.nativeElement, 'height', 'auto');
}
}
// ── Input aliasing — attribute name differs from TypeScript property ───────
@Directive({
selector: '[appTooltip]',
standalone: true,
})
export class TooltipDirective {
// Attribute: [appTooltip]="'message'" → TypeScript: this.message
@Input({ alias: 'appTooltip' }) message = '';
// Additional inputs use their own names
@Input() tooltipDelay = 300;
@Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
}
// Usage: <button [appTooltip]="'Save post'" tooltipDelay="500">Save</button>
alias option on @Input() decouples the HTML attribute name from the TypeScript property name. Without an alias, @Input() appHighlight forces the TypeScript property to match the long attribute name. With @Input({ alias: 'appHighlight' }) color = '', the HTML attribute is [appHighlight]="'yellow'" but the TypeScript property is the clean name color. This is especially useful for directive selectors that follow the appXxx naming convention but need clean internal property names.exportAs metadata lets components access the directive instance directly in the template via a template reference variable. This is how Angular Material exposes its directives: #picker="matDatepicker" gives the template direct access to the MatDatepicker instance to call picker.open(). Use exportAs when your directive has public methods the template should be able to call — it is cleaner than communicating through event bindings for imperative operations.TemplateRef and ViewContainerRef (like HasRoleDirective), it functions as a structural directive even without the * prefix. Structural directives that manipulate the view container should be used with the * prefix for clarity. Without the prefix, the syntax is more verbose: [appHasRole]="'Admin'" on the element requires the directive to explicitly use the element’s ViewContainerRef, which may cause unexpected results if the element itself is also a component.Common Mistakes
Mistake 1 — Forgetting to export directive from standalone component imports
❌ Wrong — using [appTooltip] in a template without adding TooltipDirective to the component’s imports: []; NG8001 error.
✅ Correct — add every directive used in a template to the standalone component’s imports: [] array.
Mistake 2 — Not cleaning up ViewContainerRef on changes (stale embedded views)
❌ Wrong — calling viewContainer.createEmbeddedView() on each role change without first calling viewContainer.clear(); duplicate views accumulate.
✅ Correct — always call viewContainer.clear() before conditionally creating a new embedded view.