+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 171 of 355

📘 Angular Directives: Custom Directives

Master angular directives: custom directives in TypeScript with practical examples, best practices, and real-world applications 🚀

🚀Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript 📝
  • TypeScript installation ⚡
  • VS Code or preferred IDE 💻

What you'll learn

  • Understand Angular directive fundamentals 🎯
  • Apply custom directives in real projects 🏗️
  • Debug common directive issues 🐛
  • Write type-safe directive code ✨

🎯 Introduction

Welcome to this exciting tutorial on Angular Custom Directives! 🎉 In this guide, we’ll explore how to create powerful, reusable directives that enhance your Angular applications with TypeScript.

You’ll discover how custom directives can transform your Angular development experience. Whether you’re building interactive web applications 🌐, creating reusable components 🧩, or adding custom behaviors to DOM elements 🎨, understanding custom directives is essential for writing robust, maintainable Angular code.

By the end of this tutorial, you’ll feel confident creating your own custom directives in Angular projects! Let’s dive in! 🏊‍♂️

📚 Understanding Angular Directives

🤔 What are Angular Directives?

Angular directives are like magical instructions 🪄 that tell Angular how to transform the DOM. Think of them as special markers that Angular recognizes and says “Hey, I need to do something special here!”

In TypeScript terms, directives are classes decorated with @Directive() that extend the functionality of HTML elements ✨. This means you can:

  • ✨ Add custom behaviors to any element
  • 🚀 Create reusable functionality across components
  • 🛡️ Encapsulate complex DOM manipulations
  • 🎯 Build interactive user interfaces

💡 Why Create Custom Directives?

Here’s why developers love custom directives:

  1. Reusability 🔄: Write once, use everywhere
  2. Separation of Concerns 📦: Keep logic separate from templates
  3. DOM Manipulation 🎨: Safely interact with the DOM
  4. Type Safety 🔒: Full TypeScript support with strong typing
  5. Testing 🧪: Easy to unit test directive logic

Real-world example: Imagine building a tooltip system 💬. With custom directives, you can add tooltips to any element with a simple attribute!

🔧 Basic Syntax and Usage

📝 Simple Directive Example

Let’s start with a friendly example:

// 👋 Hello, Angular Directive!
import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[appHighlight]' // 🎯 Attribute selector
})
export class HighlightDirective implements OnInit {
  
  constructor(private el: ElementRef) {} // 🏗️ Inject ElementRef
  
  ngOnInit(): void {
    // 🎨 Add yellow background to the element
    this.el.nativeElement.style.backgroundColor = 'yellow';
    console.log('🌟 Element highlighted!');
  }
}

💡 Explanation: This directive adds a yellow background to any element that has the appHighlight attribute. The ElementRef gives us access to the DOM element!

🎯 Using the Directive

Here’s how to use it in templates:

<!-- 🎨 Apply the directive to elements -->
<p appHighlight>This paragraph will be highlighted! ✨</p>
<div appHighlight>This div too! 🎉</div>
<span appHighlight>And this span! 🌟</span>

Module Registration:

// 📦 Don't forget to declare in your module!
@NgModule({
  declarations: [
    HighlightDirective, // 🎯 Add your directive here
    // ... other components
  ]
})
export class AppModule { }

💡 Practical Examples

🛒 Example 1: Interactive Color Picker Directive

Let’s build something useful:

// 🎨 Color picker directive
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appColorPicker]'
})
export class ColorPickerDirective {
  @Input() appColorPicker: string = '#ff6b6b'; // 🎨 Default color
  @Input() hoverColor: string = '#4ecdc4'; // 💫 Hover color
  
  private originalColor: string = '';
  
  constructor(private el: ElementRef) {}
  
  ngOnInit(): void {
    // 🎯 Store original color
    this.originalColor = this.el.nativeElement.style.backgroundColor || 'transparent';
    this.el.nativeElement.style.backgroundColor = this.appColorPicker;
  }
  
  // 🖱️ Handle mouse enter
  @HostListener('mouseenter') onMouseEnter(): void {
    this.el.nativeElement.style.backgroundColor = this.hoverColor;
    this.el.nativeElement.style.transform = 'scale(1.05)';
    this.el.nativeElement.style.transition = 'all 0.3s ease';
    console.log('🎉 Hover effect activated!');
  }
  
  // 🖱️ Handle mouse leave
  @HostListener('mouseleave') onMouseLeave(): void {
    this.el.nativeElement.style.backgroundColor = this.appColorPicker;
    this.el.nativeElement.style.transform = 'scale(1)';
    console.log('👋 Hover effect deactivated!');
  }
  
  // 🎯 Handle click
  @HostListener('click') onClick(): void {
    const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24', '#f0932b'];
    const randomColor = colors[Math.floor(Math.random() * colors.length)];
    this.appColorPicker = randomColor;
    this.el.nativeElement.style.backgroundColor = randomColor;
    console.log(`🎨 New color: ${randomColor}`);
  }
}

🎮 Using the Color Picker:

<!-- 🎨 Basic usage -->
<div appColorPicker>Click me to change colors! 🎨</div>

<!-- 🎯 With custom colors -->
<button 
  appColorPicker="#purple" 
  hoverColor="#violet">
  Purple Button! 💜
</button>

<!-- 🎪 Multiple elements -->
<div class="card-container">
  <div appColorPicker="#ff6b6b" hoverColor="#ff5252">Card 1 🎴</div>
  <div appColorPicker="#4ecdc4" hoverColor="#26d0ce">Card 2 🎴</div>
  <div appColorPicker="#45b7d1" hoverColor="#2196f3">Card 3 🎴</div>
</div>

🎮 Example 2: Animated Click Effect Directive

Let’s make it more interactive:

// ✨ Ripple effect directive
import { Directive, ElementRef, HostListener, Renderer2 } from '@angular/core';

@Directive({
  selector: '[appRipple]'
})
export class RippleDirective {
  
  constructor(
    private el: ElementRef,
    private renderer: Renderer2 // 🎯 Safer DOM manipulation
  ) {}
  
  @HostListener('click', ['$event']) onClick(event: MouseEvent): void {
    // 🎯 Create ripple effect
    this.createRipple(event);
  }
  
  private createRipple(event: MouseEvent): void {
    const element = this.el.nativeElement;
    const rect = element.getBoundingClientRect();
    
    // 📏 Calculate click position
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    
    // 🎨 Create ripple element
    const ripple = this.renderer.createElement('span');
    this.renderer.addClass(ripple, 'ripple-effect');
    
    // 📐 Set position and size
    this.renderer.setStyle(ripple, 'left', `${x}px`);
    this.renderer.setStyle(ripple, 'top', `${y}px`);
    this.renderer.setStyle(ripple, 'position', 'absolute');
    this.renderer.setStyle(ripple, 'border-radius', '50%');
    this.renderer.setStyle(ripple, 'background-color', 'rgba(255, 255, 255, 0.6)');
    this.renderer.setStyle(ripple, 'transform', 'scale(0)');
    this.renderer.setStyle(ripple, 'animation', 'ripple-animation 0.6s linear');
    this.renderer.setStyle(ripple, 'pointer-events', 'none');
    
    // 🎯 Make parent element relative if needed
    if (window.getComputedStyle(element).position === 'static') {
      this.renderer.setStyle(element, 'position', 'relative');
    }
    
    // 🎪 Add overflow hidden for clean effect
    this.renderer.setStyle(element, 'overflow', 'hidden');
    
    // ➕ Add ripple to element
    this.renderer.appendChild(element, ripple);
    
    console.log('💫 Ripple effect created!');
    
    // 🗑️ Remove ripple after animation
    setTimeout(() => {
      this.renderer.removeChild(element, ripple);
      console.log('🧹 Ripple cleaned up!');
    }, 600);
  }
}

CSS for Ripple Animation:

/* 🎨 Add this to your global styles */
@keyframes ripple-animation {
  0% {
    transform: scale(0);
    opacity: 1;
  }
  50% {
    transform: scale(2);
    opacity: 0.5;
  }
  100% {
    transform: scale(4);
    opacity: 0;
  }
}

🚀 Advanced Concepts

🧙‍♂️ Advanced Topic 1: Directive with Service Injection

When you’re ready to level up, try this advanced pattern:

// 🎯 Advanced directive with service injection
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';

// 🏗️ Service for managing themes
@Injectable({
  providedIn: 'root'
})
export class ThemeService {
  private themeSubject = new Subject<string>();
  public theme$ = this.themeSubject.asObservable();
  
  private currentTheme: string = 'light';
  
  setTheme(theme: string): void {
    this.currentTheme = theme;
    this.themeSubject.next(theme);
    console.log(`🎨 Theme changed to: ${theme}`);
  }
  
  getCurrentTheme(): string {
    return this.currentTheme;
  }
}

// 🎯 Theme-aware directive
@Directive({
  selector: '[appThemeAware]'
})
export class ThemeAwareDirective implements OnInit, OnDestroy {
  @Input() lightTheme: string = '#ffffff';
  @Input() darkTheme: string = '#2d3748';
  
  private destroy$ = new Subject<void>();
  
  constructor(
    private el: ElementRef,
    private themeService: ThemeService
  ) {}
  
  ngOnInit(): void {
    // 🎯 Subscribe to theme changes
    this.themeService.theme$
      .pipe(takeUntil(this.destroy$))
      .subscribe(theme => {
        this.applyTheme(theme);
      });
    
    // 🎨 Apply initial theme
    this.applyTheme(this.themeService.getCurrentTheme());
  }
  
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
  
  private applyTheme(theme: string): void {
    const color = theme === 'dark' ? this.darkTheme : this.lightTheme;
    this.el.nativeElement.style.backgroundColor = color;
    this.el.nativeElement.style.color = theme === 'dark' ? '#ffffff' : '#000000';
    console.log(`🎨 Applied ${theme} theme with color ${color}`);
  }
}

🏗️ Advanced Topic 2: Structural Directive

For the brave developers:

// 🚀 Custom structural directive
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]'  // 🎯 Opposite of *ngIf
})
export class UnlessDirective {
  private hasView = false;
  
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}
  
  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      // 🎯 Show template when condition is false
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
      console.log('✨ Template shown!');
    } else if (condition && this.hasView) {
      // 🗑️ Hide template when condition is true
      this.viewContainer.clear();
      this.hasView = false;
      console.log('👻 Template hidden!');
    }
  }
}

Usage:

<!-- 🎯 Show content when user is NOT logged in -->
<div *appUnless="isLoggedIn">
  <p>Please log in to continue! 🔐</p>
</div>

⚠️ Common Pitfalls and Solutions

😱 Pitfall 1: Direct DOM Manipulation

// ❌ Wrong way - unsafe DOM access!
@Directive({
  selector: '[appDangerous]'
})
export class DangerousDirective {
  ngOnInit(): void {
    // 💥 This breaks server-side rendering!
    document.getElementById('myElement').style.color = 'red';
  }
}

// ✅ Correct way - use ElementRef and Renderer2!
@Directive({
  selector: '[appSafe]'
})
export class SafeDirective {
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}
  
  ngOnInit(): void {
    // ✅ Safe DOM manipulation
    this.renderer.setStyle(this.el.nativeElement, 'color', 'red');
    console.log('🛡️ Safe DOM manipulation!');
  }
}

🤯 Pitfall 2: Memory Leaks with Event Listeners

// ❌ Dangerous - memory leak!
@Directive({
  selector: '[appLeaky]'
})
export class LeakyDirective implements OnInit {
  ngOnInit(): void {
    // 💧 This listener never gets cleaned up!
    window.addEventListener('resize', this.handleResize);
  }
  
  handleResize(): void {
    console.log('Window resized!');
  }
}

// ✅ Safe - proper cleanup!
@Directive({
  selector: '[appClean]'
})
export class CleanDirective implements OnInit, OnDestroy {
  private resizeListener = this.handleResize.bind(this);
  
  ngOnInit(): void {
    window.addEventListener('resize', this.resizeListener);
  }
  
  ngOnDestroy(): void {
    // 🧹 Clean up the listener!
    window.removeEventListener('resize', this.resizeListener);
    console.log('🧹 Cleaned up event listener!');
  }
  
  private handleResize(): void {
    console.log('Window resized safely! ✅');
  }
}

🛠️ Best Practices

  1. 🎯 Use Specific Selectors: Don’t use generic selectors like [myDirective]
  2. 🛡️ Use Renderer2: Always prefer Renderer2 over direct DOM manipulation
  3. 🧹 Clean Up: Always implement OnDestroy for cleanup
  4. 📝 Type Everything: Use TypeScript interfaces for input types
  5. ✨ Keep It Simple: One responsibility per directive
  6. 🧪 Test Your Directives: Write unit tests for directive logic

🧪 Hands-On Exercise

🎯 Challenge: Build a Tooltip Directive

Create a comprehensive tooltip system:

📋 Requirements:

  • ✅ Show tooltip on hover with custom text
  • 🎨 Customizable position (top, bottom, left, right)
  • 💫 Smooth fade-in/fade-out animations
  • 📱 Mobile-friendly (show on tap for touch devices)
  • 🎯 Type-safe configuration options
  • 🧹 Proper cleanup when directive is destroyed

🚀 Bonus Points:

  • Add arrow pointing to the target element
  • Support HTML content in tooltips
  • Implement automatic positioning to avoid viewport edges
  • Add delay options for show/hide

💡 Solution

🔍 Click to see solution
// 🎯 Our comprehensive tooltip directive!
import { 
  Directive, 
  ElementRef, 
  HostListener, 
  Input, 
  OnDestroy, 
  Renderer2,
  OnInit
} from '@angular/core';

interface TooltipConfig {
  text: string;
  position: 'top' | 'bottom' | 'left' | 'right';
  delay: number;
  theme: 'dark' | 'light';
}

@Directive({
  selector: '[appTooltip]'
})
export class TooltipDirective implements OnInit, OnDestroy {
  @Input() appTooltip: string = ''; // 💬 Tooltip text
  @Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top';
  @Input() tooltipDelay: number = 500; // ⏱️ Delay in ms
  @Input() tooltipTheme: 'dark' | 'light' = 'dark';
  
  private tooltipElement: HTMLElement | null = null;
  private showTimeout: any;
  private hideTimeout: any;
  
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}
  
  ngOnInit(): void {
    // 🎯 Set up the host element
    this.renderer.setStyle(this.el.nativeElement, 'position', 'relative');
  }
  
  @HostListener('mouseenter') onMouseEnter(): void {
    this.showTooltip();
  }
  
  @HostListener('mouseleave') onMouseLeave(): void {
    this.hideTooltip();
  }
  
  @HostListener('touchstart') onTouchStart(): void {
    // 📱 Mobile support
    this.showTooltip();
  }
  
  @HostListener('touchend') onTouchEnd(): void {
    // 📱 Hide after touch
    setTimeout(() => this.hideTooltip(), 2000);
  }
  
  private showTooltip(): void {
    if (this.hideTimeout) {
      clearTimeout(this.hideTimeout);
    }
    
    this.showTimeout = setTimeout(() => {
      this.createTooltip();
    }, this.tooltipDelay);
  }
  
  private hideTooltip(): void {
    if (this.showTimeout) {
      clearTimeout(this.showTimeout);
    }
    
    if (this.tooltipElement) {
      this.hideTimeout = setTimeout(() => {
        this.removeTooltip();
      }, 100);
    }
  }
  
  private createTooltip(): void {
    if (this.tooltipElement || !this.appTooltip) return;
    
    // 🎨 Create tooltip element
    this.tooltipElement = this.renderer.createElement('div');
    this.renderer.addClass(this.tooltipElement, 'custom-tooltip');
    this.renderer.addClass(this.tooltipElement, `tooltip-${this.tooltipTheme}`);
    this.renderer.addClass(this.tooltipElement, `tooltip-${this.tooltipPosition}`);
    
    // 📝 Set content
    const textNode = this.renderer.createText(this.appTooltip);
    this.renderer.appendChild(this.tooltipElement, textNode);
    
    // 🎯 Position tooltip
    this.setTooltipStyles();
    
    // ➕ Add to DOM
    this.renderer.appendChild(this.el.nativeElement, this.tooltipElement);
    
    // ✨ Animate in
    setTimeout(() => {
      this.renderer.addClass(this.tooltipElement, 'tooltip-visible');
    }, 10);
    
    console.log('💬 Tooltip shown!');
  }
  
  private setTooltipStyles(): void {
    if (!this.tooltipElement) return;
    
    // 🎨 Base styles
    this.renderer.setStyle(this.tooltipElement, 'position', 'absolute');
    this.renderer.setStyle(this.tooltipElement, 'padding', '8px 12px');
    this.renderer.setStyle(this.tooltipElement, 'border-radius', '4px');
    this.renderer.setStyle(this.tooltipElement, 'font-size', '14px');
    this.renderer.setStyle(this.tooltipElement, 'white-space', 'nowrap');
    this.renderer.setStyle(this.tooltipElement, 'z-index', '1000');
    this.renderer.setStyle(this.tooltipElement, 'opacity', '0');
    this.renderer.setStyle(this.tooltipElement, 'transition', 'opacity 0.3s ease');
    this.renderer.setStyle(this.tooltipElement, 'pointer-events', 'none');
    
    // 🎨 Theme styles
    if (this.tooltipTheme === 'dark') {
      this.renderer.setStyle(this.tooltipElement, 'background-color', '#333');
      this.renderer.setStyle(this.tooltipElement, 'color', '#fff');
    } else {
      this.renderer.setStyle(this.tooltipElement, 'background-color', '#fff');
      this.renderer.setStyle(this.tooltipElement, 'color', '#333');
      this.renderer.setStyle(this.tooltipElement, 'border', '1px solid #ccc');
    }
    
    // 📐 Position styles
    switch (this.tooltipPosition) {
      case 'top':
        this.renderer.setStyle(this.tooltipElement, 'bottom', '100%');
        this.renderer.setStyle(this.tooltipElement, 'left', '50%');
        this.renderer.setStyle(this.tooltipElement, 'transform', 'translateX(-50%)');
        this.renderer.setStyle(this.tooltipElement, 'margin-bottom', '5px');
        break;
      case 'bottom':
        this.renderer.setStyle(this.tooltipElement, 'top', '100%');
        this.renderer.setStyle(this.tooltipElement, 'left', '50%');
        this.renderer.setStyle(this.tooltipElement, 'transform', 'translateX(-50%)');
        this.renderer.setStyle(this.tooltipElement, 'margin-top', '5px');
        break;
      case 'left':
        this.renderer.setStyle(this.tooltipElement, 'right', '100%');
        this.renderer.setStyle(this.tooltipElement, 'top', '50%');
        this.renderer.setStyle(this.tooltipElement, 'transform', 'translateY(-50%)');
        this.renderer.setStyle(this.tooltipElement, 'margin-right', '5px');
        break;
      case 'right':
        this.renderer.setStyle(this.tooltipElement, 'left', '100%');
        this.renderer.setStyle(this.tooltipElement, 'top', '50%');
        this.renderer.setStyle(this.tooltipElement, 'transform', 'translateY(-50%)');
        this.renderer.setStyle(this.tooltipElement, 'margin-left', '5px');
        break;
    }
  }
  
  private removeTooltip(): void {
    if (this.tooltipElement) {
      this.renderer.removeChild(this.el.nativeElement, this.tooltipElement);
      this.tooltipElement = null;
      console.log('🧹 Tooltip removed!');
    }
  }
  
  ngOnDestroy(): void {
    // 🧹 Clean up timers and tooltip
    if (this.showTimeout) clearTimeout(this.showTimeout);
    if (this.hideTimeout) clearTimeout(this.hideTimeout);
    this.removeTooltip();
    console.log('🧹 Tooltip directive destroyed!');
  }
}

CSS Styles:

/* 🎨 Add these styles to your global CSS */
.custom-tooltip.tooltip-visible {
  opacity: 1 !important;
}

.custom-tooltip::before {
  content: '';
  position: absolute;
  width: 0;
  height: 0;
  border-style: solid;
}

/* 🎯 Arrow styles for different positions */
.tooltip-top::before {
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border-width: 5px 5px 0 5px;
  border-color: #333 transparent transparent transparent;
}

.tooltip-bottom::before {
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  border-width: 0 5px 5px 5px;
  border-color: transparent transparent #333 transparent;
}

Usage:

<!-- 🎯 Basic usage -->
<button appTooltip="Click me to save! 💾">Save</button>

<!-- 🎨 Custom positioning and theme -->
<div 
  appTooltip="User profile settings ⚙️"
  tooltipPosition="right"
  tooltipTheme="light"
  tooltipDelay="200">
  Profile
</div>

🎓 Key Takeaways

You’ve learned so much! Here’s what you can now do:

  • Create custom directives with confidence 💪
  • Handle DOM manipulation safely using Renderer2 🛡️
  • Implement event listeners with proper cleanup 🧹
  • Use dependency injection in directives 🎯
  • Build reusable UI behaviors for Angular apps! 🚀

Remember: Custom directives are your secret weapon for creating amazing user experiences in Angular! 🤝

🤝 Next Steps

Congratulations! 🎉 You’ve mastered Angular Custom Directives!

Here’s what to do next:

  1. 💻 Practice with the tooltip exercise above
  2. 🏗️ Build a custom directive for your current project
  3. 📚 Explore Angular’s built-in directives source code
  4. 🌟 Share your custom directives with the Angular community!

Remember: Every Angular expert was once a beginner. Keep coding, keep learning, and most importantly, have fun building amazing Angular applications! 🚀


Happy coding! 🎉🚀✨