install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/angular-directive-pattern" ~/.claude/skills/intense-visions-harness-engineering-angular-directive-pattern && rm -rf "$T"
manifest:
agents/skills/claude-code/angular-directive-pattern/SKILL.mdsource content
Angular Directive Pattern
Create attribute and structural directives with @Directive to add behavior, handle host events, and conditionally render DOM without modifying component templates
When to Use
- Adding reusable behavior to elements without wrapping them in a component (tooltip, click-outside, auto-focus, ripple)
- Conditionally rendering or structurally manipulating the DOM (custom
,*ngIf
)*appRole - Binding CSS classes, styles, or ARIA attributes to host elements reactively
- Abstracting interaction patterns (drag handles, keyboard navigation, form field enhancement)
- Creating a composable behavior that can be applied to any element via a CSS selector
Instructions
- Use
for attribute directives. Use attribute selectors (@Directive({ selector: '[appHighlight]', standalone: true })
) by convention — element selectors are reserved for components.[appX] - Use
in the decorator instead ofhost: { '(click)': 'onClick()', '[class.active]': 'isActive' }
and@HostListener
— it is compile-time verified and more readable.@HostBinding - Inject
to access the host DOM element. InjectElementRef<HTMLElement>
when manipulating the DOM to preserve server-side rendering compatibility — never manipulateRenderer2
directly in SSR.nativeElement - Accept configuration via
orinput()
. Use@Input()
for mandatory config.input.required() - Clean up event listeners and subscriptions in
. PreferngOnDestroy
over implementinginject(DestroyRef)
.OnDestroy - For structural directives, inject
andTemplateRef
. CallViewContainerRef
to render andviewContainer.createEmbeddedView(templateRef)
to remove.viewContainer.clear() - Compose directives — a single element can have multiple attribute directives applied simultaneously.
- Add the standalone directive to the
array of the components that use it.imports
// 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>
Details
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>.
vs host
/@HostListener
: The @HostBinding
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));
Source
https://angular.dev/guide/directives
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.