Directives

Documentation for Developers

📐 Angular Directive Development Guidelines

Modern best practices for creating Angular 20 directives that work flawlessly with IDE support


🎯 Essential Rules for Directives

1. Use Explicit @Input() Properties (Not Setters)

// ✅ CORRECT - Language server recognizes these
@Input() myDirectiveProp: string = 'default';
@Input() myDirectiveEnabled: boolean = true;
@Input() myDirectiveTrigger: any = null;

// ❌ AVOID - Language server struggles
@Input() set myDirectiveProp(value: string) { ... }

Why: Angular's language server and linter better recognize explicit property declarations.

2. Include All Properties in Selector

@Directive({
  selector: '[myDirective],[myDirectiveProp],[myDirectiveEnabled]',
  standalone: true
})

Why: Ensures Angular can find the directive when any property is used.

3. Use Signals for Reactivity

export class MyDirective {
  // Public @Input() for Angular
  @Input() myDirective: string = 'default';
  
  // Private signal for reactivity
  private readonly state = signal('default');
  
  constructor() {
    // Sync inputs to signals
    effect(() => {
      this.state.set(this.myDirective);
    });
    
    // React to state changes
    effect(() => {
      const value = this.state();
      // Do something with value
    });
  }
}

📦 Complete Directive Template

Modern Angular 20 Directive Pattern

import { 
  Directive, 
  Input, 
  ElementRef, 
  inject, 
  signal, 
  effect,
  OnInit, 
  OnDestroy 
} from '@angular/core';

@Directive({
  selector: '[myAwesomeDirective],[myAwesomeDirectiveSpeed],[myAwesomeDirectiveEnabled]',
  standalone: true,
  host: {
    '[attr.data-my-directive]': 'true'
  }
})
export class MyAwesomeDirective implements OnInit, OnDestroy {
  private readonly elementRef = inject(ElementRef);
  
  // ✅ Explicit @Input() properties (language server friendly)
  @Input() myAwesomeDirective: string = 'default';
  @Input() myAwesomeDirectiveSpeed: 'fast' | 'normal' | 'slow' = 'normal';
  @Input() myAwesomeDirectiveEnabled: boolean = true;
  @Input() myAwesomeDirectiveTrigger: any = null;
  
  // Private reactive signals
  private readonly state = signal('default');
  private readonly speed = signal<'fast' | 'normal' | 'slow'>('normal');
  private readonly enabled = signal(true);
  private readonly trigger = signal<any>(null);
  
  constructor() {
    // Sync inputs to signals (reactive bridge)
    effect(() => {
      this.state.set(this.myAwesomeDirective);
      this.speed.set(this.myAwesomeDirectiveSpeed);
      this.enabled.set(this.myAwesomeDirectiveEnabled);
      this.trigger.set(this.myAwesomeDirectiveTrigger);
    });
    
    // React to changes
    effect(() => {
      if (this.enabled()) {
        this.applyDirectiveBehavior();
      }
    });
  }
  
  ngOnInit(): void {
    // Initial setup
    this.applyDirectiveBehavior();
  }
  
  ngOnDestroy(): void {
    // Cleanup
  }
  
  private applyDirectiveBehavior(): void {
    // Your directive logic here
    const element = this.elementRef.nativeElement;
    // ...
  }
}

🎨 Property Naming Conventions

Consistent Prefix Pattern

// Library: flash-animations
@Input() flashAnim: string;           // Main property
@Input() flashAnimSpeed: string;     // Sub-property
@Input() flashAnimEnabled: boolean;  // Sub-property

// Library: my-ui-lib  
@Input() myUi: string;                // Main property
@Input() myUiTheme: string;          // Sub-property
@Input() myUiEnabled: boolean;       // Sub-property

Benefits:

Type Safety

// ✅ Use union types for controlled values
@Input() animationType: 'fade' | 'slide' | 'scale' = 'fade';

// ✅ Use optional for nullable values
@Input() customDuration?: number;

// ✅ Use explicit types for booleans
@Input() enabled: boolean = true;

📊 Selector Best Practices

Multi-Property Selectors

@Directive({
  selector: '[mainDirective],[mainDirectiveSpeed],[mainDirectiveEnabled]',
  standalone: true
})

Why: Allows directive to be found regardless of which property is used first.

Host Bindings

@Directive({
  host: {
    '[attr.data-directive-name]': 'true',
    '[class.directive-active]': 'enabled'
  }
})

Benefits:


🔧 Testing & Building

After Creating Directive

  1. Build library:

    yarn nx build [library-name]
    
  2. Test in app component:

    imports: [YourDirective]
    
  3. If linter errors persist:

    • Restart TypeScript Language Server
    • OR restart dev server
    • OR ignore if functionality works

Common Issues & Solutions

Issue Cause Solution
"Can't bind to property" Old library cache Rebuild + restart TS server
Directive not found Missing import Add to component imports
Properties not recognized Setter syntax Use explicit @Input() properties
Type errors Mismatched types Use proper TypeScript types

🎯 Flash Animations Example

This is exactly how we built the flash-animations directives:

@Directive({
  selector: '[flashAnim],[flashAnimTrigger],[flashAnimSpeed]',
  standalone: true
})
export class FlashAnimateDirective {
  // ✅ Explicit properties
  @Input() flashAnim: FlashAnimationPreset = 'fade-in';
  @Input() flashAnimSpeed: FlashAnimationSpeed = 'normal';
  @Input() flashAnimTrigger: any = null;
  
  // ✅ Reactive signals
  private readonly preset = signal<FlashAnimationPreset>('fade-in');
  
  constructor() {
    // ✅ Bridge inputs to signals
    effect(() => {
      this.preset.set(this.flashAnim);
    });
  }
}

Usage:

<!-- ✅ Works without linter errors -->
<div [flashAnim]="'fade-in'" 
     [flashAnimSpeed]="'fast'"
     [flashAnimTrigger]="trigger">

📚 Key Takeaways

Modern Angular 20 Patterns

  1. ✅ Standalone directives (standalone: true)
  2. ✅ Signal-based reactivity (private signals + effects)
  3. ✅ Explicit @Input() properties (not setters)
  4. ✅ Multi-property selectors (comprehensive coverage)
  5. ✅ Host bindings (debugging and styling)
  6. ✅ Type safety (union types, optional properties)

IDE & Language Server Compatibility

  1. ✅ Use explicit property declarations
  2. ✅ Include all properties in selector
  3. ✅ Export from public API properly
  4. ✅ Rebuild library after changes
  5. ✅ Restart TS server if needed

Developer Experience

  1. ✅ Consistent naming conventions
  2. ✅ Clear property grouping (IDE autocomplete)
  3. ✅ Helpful defaults
  4. ✅ Comprehensive TypeScript support
  5. ✅ No linter errors

🚀 Quick Checklist

When creating a new directive:


Result: Directives that work flawlessly with full IDE support! ✨


Created: October 3, 2025
Based on: Flash Animations directive development experience
Status: ✅ Battle-tested guidelines