From harness-claude
Creates attribute and structural directives with @Directive to add behavior, handle host events, and conditionally render DOM without modifying component templates.
How this skill is triggered — by the user, by Claude, or both
Slash command
/harness-claude:angular-directive-patternThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Create attribute and structural directives with @Directive to add behavior, handle host events, and conditionally render DOM without modifying component templates
Create attribute and structural directives with @Directive to add behavior, handle host events, and conditionally render DOM without modifying component templates
*ngIf, *appRole)@Directive({ selector: '[appHighlight]', standalone: true }) for attribute directives. Use attribute selectors ([appX]) by convention — element selectors are reserved for components.host: { '(click)': 'onClick()', '[class.active]': 'isActive' } in the decorator instead of @HostListener and @HostBinding — it is compile-time verified and more readable.ElementRef<HTMLElement> to access the host DOM element. Inject Renderer2 when manipulating the DOM to preserve server-side rendering compatibility — never manipulate nativeElement directly in SSR.input() or @Input(). Use input.required() for mandatory config.ngOnDestroy. Prefer inject(DestroyRef) over implementing OnDestroy.TemplateRef and ViewContainerRef. Call viewContainer.createEmbeddedView(templateRef) to render and viewContainer.clear() to remove.imports array of the components that use it.// highlight.directive.ts — attribute directive
import { Directive, ElementRef, Renderer2, input, inject, DestroyRef } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true,
host: {
'(mouseenter)': 'onEnter()',
'(mouseleave)': 'onLeave()',
'[style.backgroundColor]': 'highlightColor()',
},
})
export class HighlightDirective {
private el = inject(ElementRef<HTMLElement>);
private renderer = inject(Renderer2);
color = input<string>('yellow', { alias: 'appHighlight' });
highlightColor = computed(() => (this.active ? this.color() : 'transparent'));
private active = false;
onEnter(): void {
this.active = true;
}
onLeave(): void {
this.active = false;
}
}
// click-outside.directive.ts — global event on document
import { Directive, output, inject, ElementRef, OnInit, DestroyRef } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { fromEvent } from 'rxjs';
@Directive({ selector: '[appClickOutside]', standalone: true })
export class ClickOutsideDirective implements OnInit {
clickOutside = output<void>();
private el = inject(ElementRef);
private document = inject(DOCUMENT);
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
fromEvent<MouseEvent>(this.document, 'click')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
if (!this.el.nativeElement.contains(event.target)) {
this.clickOutside.emit();
}
});
}
}
// role.directive.ts — structural directive
import { Directive, inject, input, TemplateRef, ViewContainerRef, effect } from '@angular/core';
import { AuthService } from './auth.service';
@Directive({ selector: '[appRole]', standalone: true })
export class RoleDirective {
private auth = inject(AuthService);
private templateRef = inject(TemplateRef<unknown>);
private viewContainer = inject(ViewContainerRef);
requiredRole = input.required<string>({ alias: 'appRole' });
constructor() {
effect(() => {
if (this.auth.hasRole(this.requiredRole())) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
});
}
}
<!-- Usage -->
<p appHighlight="lightblue">Hover me</p>
<div [appClickOutside]="close()" (appClickOutside)="isOpen = false">...</div>
<button *appRole="'admin'">Delete All</button>
Attribute vs structural directives: Attribute directives add or modify behavior on an existing element (classes, styles, events, ARIA). Structural directives change the DOM layout by adding or removing elements — they use the * prefix sugar which desugars to [ngTemplateOutlet] with a <ng-template>.
host vs @HostListener/@HostBinding: The host metadata in the decorator is the preferred approach in Angular 17+. It is statically analyzable, doesn't require decorator imports in the class body, and maps directly to what the compiler emits. @HostListener and @HostBinding still work but are considered legacy style.
Renderer2 for SSR safety: Direct DOM manipulation (this.el.nativeElement.style.color = 'red') breaks in server-side rendering because document and DOM APIs don't exist. Renderer2 abstracts the rendering layer and works in both browser and Node environments:
this.renderer.setStyle(this.el.nativeElement, 'color', 'red');
this.renderer.addClass(this.el.nativeElement, 'is-active');
this.renderer.setAttribute(this.el.nativeElement, 'aria-expanded', 'true');
TypeScript context type for structural directives: Provide a static ngTemplateContextGuard to type the template variables exposed by a structural directive:
static ngTemplateContextGuard<T>(
dir: LetDirective<T>,
ctx: unknown
): ctx is { appLet: T } { return true; }
Testing directives:
@Component({ template: '<span appHighlight="yellow">Test</span>', imports: [HighlightDirective] })
class TestHostComponent {}
const fixture = TestBed.createComponent(TestHostComponent);
const directive = fixture.debugElement.query(By.directive(HighlightDirective));
https://angular.dev/guide/directives
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeCreate custom directives in Angular v20+ for DOM manipulation and behavior extension. Covers attribute directives, structural directives for portals/overlays, and host directives for composition.
Creates custom Vue directives for reusable DOM-level behaviors like auto-focus, click-outside, intersection observer, and tooltips.
Builds token-driven Angular components with signals and standalone components. Use when creating Angular component libraries, integrating design tokens, or building design system components with new control flow syntax.