From harness-claude
Creates Angular attribute and structural directives to add reusable behaviors like tooltips and click-outside, handle host events, bind styles/classes, and conditionally render DOM.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Create attribute and structural directives with @Directive to add behavior, handle host events, and conditionally render DOM without modifying component templates
Creates custom Angular v20+ directives for DOM manipulation: attribute directives for behavior/appearance changes, structural for portals/overlays, host for composition. Use for reusable element behaviors.
Create custom Vue directives for low-level DOM manipulation and reusable behaviors like auto-focus, click-outside, intersection observer, and tooltips. Use for DOM-level logic without component state.
Implements Angular dependency injection using providers (useClass, useValue, useFactory), injectors, and services for modular, testable applications.
Share bugs, ideas, or general feedback.
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