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:
- ✅ IDE autocomplete groups related properties
- ✅ No conflicts with other libraries
- ✅ Clear ownership
- ✅ Consistent naming
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:
- ✅ Easy CSS targeting
- ✅ Debug visibility in DOM
- ✅ State reflection
🔧 Testing & Building
After Creating Directive
Build library:
yarn nx build [library-name]Test in app component:
imports: [YourDirective]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
- ✅ Standalone directives (
standalone: true) - ✅ Signal-based reactivity (private signals + effects)
- ✅ Explicit @Input() properties (not setters)
- ✅ Multi-property selectors (comprehensive coverage)
- ✅ Host bindings (debugging and styling)
- ✅ Type safety (union types, optional properties)
IDE & Language Server Compatibility
- ✅ Use explicit property declarations
- ✅ Include all properties in selector
- ✅ Export from public API properly
- ✅ Rebuild library after changes
- ✅ Restart TS server if needed
Developer Experience
- ✅ Consistent naming conventions
- ✅ Clear property grouping (IDE autocomplete)
- ✅ Helpful defaults
- ✅ Comprehensive TypeScript support
- ✅ No linter errors
🚀 Quick Checklist
When creating a new directive:
- Use explicit
@Input()properties - Include all properties in selector
- Use signals for internal reactivity
- Add host bindings for debugging
- Use consistent naming prefix
- Add proper TypeScript types
- Test in component (add to imports)
- Build library and verify no errors
- Document usage patterns
Result: Directives that work flawlessly with full IDE support! ✨
Created: October 3, 2025
Based on: Flash Animations directive development experience
Status: ✅ Battle-tested guidelines