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:
- Reusability 🔄: Write once, use everywhere
- Separation of Concerns 📦: Keep logic separate from templates
- DOM Manipulation 🎨: Safely interact with the DOM
- Type Safety 🔒: Full TypeScript support with strong typing
- 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
- 🎯 Use Specific Selectors: Don’t use generic selectors like
[myDirective]
- 🛡️ Use Renderer2: Always prefer Renderer2 over direct DOM manipulation
- 🧹 Clean Up: Always implement OnDestroy for cleanup
- 📝 Type Everything: Use TypeScript interfaces for input types
- ✨ Keep It Simple: One responsibility per directive
- 🧪 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:
- 💻 Practice with the tooltip exercise above
- 🏗️ Build a custom directive for your current project
- 📚 Explore Angular’s built-in directives source code
- 🌟 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! 🎉🚀✨