Skip to content
Back to skills

Angular Directive Pattern

ASecurity

> Create attribute and structural directives with @Directive to add behavior, handle host events, and conditionally render DOM without modifying component templates

  • 20 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 11, 2026
developmenttypescriptgoreactangularnodetestingapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 11, 2026

npx -y skills add Intense-Visions/harness-engineering --skill angular-directive-pattern --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Angular Directive Pattern?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Angular Directive Pattern
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/intense-visions-angular-directive-pattern/badge)](https://www.skillsdirectory.com/skills/intense-visions-angular-directive-pattern)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# 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

1. Use `@Directive({ selector: '[appHighlight]', standalone: true })` for attribute directives. Use attribute selectors (`[appX]`) by convention — element selectors are reserved for components.
2. Use `host: { '(click)': 'onClick()', '[class.active]': 'isActive' }` in the decorator instead of `@HostListener` and `@HostBinding` — it is compile-time verified and more readable.
3. Inject `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.
4. Accept configuration via `input()` or `@Input()`. Use `input.required()` for mandatory config.
5. Clean up event listeners and subscriptions in `ngOnDestroy`. Prefer `inject(DestroyRef)` over implementing `OnDestroy`.
6. For structural directives, inject `TemplateRef` and `ViewContainerRef`. Call `viewContainer.createEmbeddedView(templateRef)` to render and `viewContainer.clear()` to remove.
7. Compose directives — a single element can have multiple attribute directives applied simultaneously.
8. Add the standalone directive to the `imports` array of the components that use it.

```typescript
// 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;
  }
}
```

```typescript
// 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();
        }
      });
  }
}
```

```typescript
// 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();
      }
    });
  }
}
```

```html
<!-- 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>`.

**`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:

```typescript
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:

```typescript
static ngTemplateContextGuard<T>(
  dir: LetDirective<T>,
  ctx: unknown
): ctx is { appLet: T } { return true; }
```

**Testing directives:**

```typescript
@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

1. Read the instructions and examples in this document.
2. Apply the patterns to your implementation, adapting to your specific context.
3. 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.

Files in this skill

  • SKILL.md6.7 KB
  • skill.yaml893 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…