自定义指令 #

一、指令概述 #

自定义指令分为两种类型:

类型 说明 示例
属性指令 改变元素外观或行为 高亮、焦点、权限
结构指令 改变DOM结构 权限控制、懒加载

二、创建属性指令 #

2.1 使用CLI创建 #

bash
ng generate directive directives/highlight
# 简写
ng g d directives/highlight

2.2 高亮指令示例 #

typescript
import { Directive, ElementRef, Renderer2, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input() appHighlight: string = 'yellow';
  
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}
  
  @HostListener('mouseenter')
  onMouseEnter() {
    this.highlight(this.appHighlight);
  }
  
  @HostListener('mouseleave')
  onMouseLeave() {
    this.highlight(null);
  }
  
  private highlight(color: string | null) {
    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);
  }
}

使用:

html
<p appHighlight>默认高亮</p>
<p [appHighlight]="'lightblue'">自定义颜色高亮</p>

2.3 ElementRef和Renderer2 #

typescript
import { Directive, ElementRef, Renderer2 } from '@angular/core';

@Directive({
  selector: '[appStyle]',
  standalone: true
})
export class StyleDirective {
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {
    // 使用ElementRef直接访问DOM(不推荐)
    // this.el.nativeElement.style.color = 'red';
    
    // 使用Renderer2(推荐,更安全)
    this.renderer.setStyle(this.el.nativeElement, 'color', 'red');
    this.renderer.addClass(this.el.nativeElement, 'highlighted');
    this.renderer.setAttribute(this.el.nativeElement, 'title', '提示文字');
  }
}

2.4 Renderer2常用方法 #

typescript
constructor(private renderer: Renderer2) {}

// 设置样式
renderer.setStyle(element, 'color', 'red');
renderer.removeStyle(element, 'color');

// 添加/移除类
renderer.addClass(element, 'active');
renderer.removeClass(element, 'active');

// 设置属性
renderer.setAttribute(element, 'disabled', 'true');
renderer.removeAttribute(element, 'disabled');

// 设置属性值
renderer.setProperty(element, 'value', 'new value');

// 创建/添加元素
const div = renderer.createElement('div');
renderer.appendChild(parentElement, div);

// 添加文本
const text = renderer.createText('Hello');
renderer.appendChild(element, text);

// 添加监听器
renderer.listen(element, 'click', (event) => {
  console.log('clicked', event);
});

三、HostListener和HostBinding #

3.1 @HostListener #

监听宿主元素事件:

typescript
import { Directive, HostListener } from '@angular/core';

@Directive({
  selector: '[appClickLog]',
  standalone: true
})
export class ClickLogDirective {
  @HostListener('click', ['$event'])
  onClick(event: MouseEvent) {
    console.log('元素被点击', event);
  }
  
  @HostListener('mouseenter')
  onMouseEnter() {
    console.log('鼠标进入');
  }
  
  @HostListener('mouseleave')
  onMouseLeave() {
    console.log('鼠标离开');
  }
  
  @HostListener('window:resize', ['$event'])
  onResize(event: Event) {
    console.log('窗口大小改变', event);
  }
  
  @HostListener('document:keydown', ['$event'])
  onKeyDown(event: KeyboardEvent) {
    console.log('按键按下', event.key);
  }
}

3.2 @HostBinding #

绑定宿主元素属性:

typescript
import { Directive, HostBinding, Input } from '@angular/core';

@Directive({
  selector: '[appActive]',
  standalone: true
})
export class ActiveDirective {
  @HostBinding('class.active')
  @Input()
  appActive: boolean = false;
  
  @HostBinding('style.backgroundColor')
  get backgroundColor() {
    return this.appActive ? '#e0e0e0' : 'transparent';
  }
  
  @HostBinding('attr.aria-pressed')
  get ariaPressed() {
    return this.appActive;
  }
  
  @HostBinding('class.disabled')
  @Input()
  isDisabled: boolean = false;
}

使用:

html
<button [appActive]="isActive">按钮</button>
<div [appActive]="isSelected" [isDisabled]="isDisabled">内容</div>

四、输入属性 #

4.1 多个输入属性 #

typescript
import { Directive, Input, HostBinding } from '@angular/core';

@Directive({
  selector: '[appBorder]',
  standalone: true
})
export class BorderDirective {
  @Input() appBorder: string = '1px solid #ccc';
  @Input() borderColor: string = '#ccc';
  @Input() borderRadius: string = '4px';
  
  @HostBinding('style.border')
  get border() {
    return this.appBorder;
  }
  
  @HostBinding('style.borderColor')
  get computedBorderColor() {
    return this.borderColor;
  }
  
  @HostBinding('style.borderRadius')
  get computedBorderRadius() {
    return this.borderRadius;
  }
}

使用:

html
<div appBorder borderColor="red" borderRadius="8px">
  带边框的内容
</div>

4.2 输入属性别名 #

typescript
@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input('appHighlight') highlightColor: string = 'yellow';
  
  // 使用别名后,appHighlight作为输入属性名
}

五、实用指令示例 #

5.1 自动焦点指令 #

typescript
import { Directive, OnInit, ElementRef } from '@angular/core';

@Directive({
  selector: '[appAutofocus]',
  standalone: true
})
export class AutofocusDirective implements OnInit {
  constructor(private el: ElementRef) {}
  
  ngOnInit() {
    setTimeout(() => {
      this.el.nativeElement.focus();
    }, 0);
  }
}

使用:

html
<input appAutofocus type="text" placeholder="自动获取焦点" />

5.2 防抖点击指令 #

typescript
import { Directive, Output, EventEmitter, HostListener } from '@angular/core';

@Directive({
  selector: '[appDebounceClick]',
  standalone: true
})
export class DebounceClickDirective {
  @Output() debounceClick = new EventEmitter<Event>();
  
  private timeout: any;
  private delay: number = 500;
  
  @HostListener('click', ['$event'])
  onClick(event: Event) {
    event.preventDefault();
    event.stopPropagation();
    
    if (this.timeout) {
      clearTimeout(this.timeout);
    }
    
    this.timeout = setTimeout(() => {
      this.debounceClick.emit(event);
    }, this.delay);
  }
}

使用:

html
<button (debounceClick)="onClick()">防抖按钮</button>

5.3 权限指令 #

typescript
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { AuthService } from '../services/auth.service';

@Directive({
  selector: '[appHasPermission]',
  standalone: true
})
export class HasPermissionDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private authService: AuthService
  ) {}
  
  @Input()
  set appHasPermission(permission: string) {
    if (this.authService.hasPermission(permission)) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
}

使用:

html
<button *appHasPermission="'admin'">管理员操作</button>
<div *appHasPermission="'editor'">编辑内容</div>

5.4 图片懒加载指令 #

typescript
import { Directive, ElementRef, OnInit } from '@angular/core';

@Directive({
  selector: '[appLazyLoad]',
  standalone: true
})
export class LazyLoadDirective implements OnInit {
  constructor(private el: ElementRef) {}
  
  ngOnInit() {
    const img = this.el.nativeElement as HTMLImageElement;
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const src = img.getAttribute('data-src');
          if (src) {
            img.src = src;
          }
          observer.unobserve(img);
        }
      });
    });
    
    observer.observe(img);
  }
}

使用:

html
<img appLazyLoad data-src="path/to/image.jpg" src="placeholder.jpg" />

5.5 点击外部指令 #

typescript
import { Directive, Output, EventEmitter, HostListener, ElementRef } from '@angular/core';

@Directive({
  selector: '[appClickOutside]',
  standalone: true
})
export class ClickOutsideDirective {
  @Output() clickOutside = new EventEmitter<void>();
  
  constructor(private elementRef: ElementRef) {}
  
  @HostListener('document:click', ['$event.target'])
  onClick(target: HTMLElement) {
    const clickedInside = this.elementRef.nativeElement.contains(target);
    if (!clickedInside) {
      this.clickOutside.emit();
    }
  }
}

使用:

html
<div (clickOutside)="onClose()">
  下拉菜单内容
</div>

六、创建结构指令 #

6.1 基本结构指令 #

typescript
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]',
  standalone: true
})
export class UnlessDirective {
  private hasView = false;
  
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}
  
  @Input()
  set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

使用:

html
<div *appUnless="isLoading">
  内容已加载
</div>

6.2 带上下文的结构指令 #

typescript
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

interface RepeatContext {
  $implicit: number;
  index: number;
  odd: boolean;
  even: boolean;
}

@Directive({
  selector: '[appRepeat]',
  standalone: true
})
export class RepeatDirective {
  constructor(
    private templateRef: TemplateRef<RepeatContext>,
    private viewContainer: ViewContainerRef
  ) {}
  
  @Input()
  set appRepeat(times: number) {
    this.viewContainer.clear();
    
    for (let i = 0; i < times; i++) {
      this.viewContainer.createEmbeddedView(this.templateRef, {
        $implicit: i,
        index: i,
        odd: i % 2 === 1,
        even: i % 2 === 0
      });
    }
  }
}

使用:

html
<div *appRepeat="3; let i = index; let odd = odd">
  第 {{ i + 1 }} 次 {{ odd ? '(奇数)' : '(偶数)' }}
</div>

6.3 权限结构指令 #

typescript
import { Directive, Input, TemplateRef, ViewContainerRef, Optional } from '@angular/core';
import { AuthService } from '../services/auth.service';

@Directive({
  selector: '[appIfRole]',
  standalone: true
})
export class IfRoleDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private authService: AuthService
  ) {}
  
  @Input()
  set appIfRole(role: string) {
    if (this.authService.hasRole(role)) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
      this.viewContainer.clear();
    }
  }
  
  @Input()
  set appIfRoleElse(template: TemplateRef<any>) {
    if (!this.authService.hasRole(this.currentRole)) {
      this.viewContainer.createEmbeddedView(template);
    }
  }
  
  private currentRole: string;
}

使用:

html
<div *appIfRole="'admin'">管理员内容</div>
<div *appIfRole="'user'">普通用户内容</div>

七、指令导出 #

7.1 导出指令实例 #

typescript
import { Directive, HostBinding, HostListener, Output, EventEmitter } from '@angular/core';

@Directive({
  selector: '[appToggle]',
  standalone: true,
  exportAs: 'appToggle'
})
export class ToggleDirective {
  @HostBinding('class.active')
  isActive = false;
  
  @Output() toggled = new EventEmitter<boolean>();
  
  toggle() {
    this.isActive = !this.isActive;
    this.toggled.emit(this.isActive);
  }
  
  @HostListener('click')
  onClick() {
    this.toggle();
  }
}

使用:

html
<button appToggle #toggle="appToggle" (toggled)="onToggled($event)">
  {{ toggle.isActive ? '已激活' : '未激活' }}
</button>
<button (click)="toggle.toggle()">外部切换</button>

八、指令最佳实践 #

8.1 命名规范 #

typescript
// 推荐:使用app前缀
@Directive({
  selector: '[appHighlight]'
})

// 不推荐:无前缀
@Directive({
  selector: '[highlight]'
})

8.2 使用Renderer2 #

typescript
// 推荐:使用Renderer2
constructor(private renderer: Renderer2, private el: ElementRef) {
  this.renderer.setStyle(this.el.nativeElement, 'color', 'red');
}

// 不推荐:直接操作DOM
constructor(private el: ElementRef) {
  this.el.nativeElement.style.color = 'red';
}

8.3 清理资源 #

typescript
import { Directive, OnDestroy } from '@angular/core';

@Directive({
  selector: '[appEventListener]',
  standalone: true
})
export class EventListenerDirective implements OnDestroy {
  private listeners: (() => void)[] = [];
  
  constructor(private renderer: Renderer2, private el: ElementRef) {
    const unsubscribe = this.renderer.listen(this.el.nativeElement, 'click', () => {
      console.log('clicked');
    });
    this.listeners.push(unsubscribe);
  }
  
  ngOnDestroy() {
    this.listeners.forEach(unsubscribe => unsubscribe());
  }
}

九、总结 #

概念 说明
属性指令 改变元素外观或行为
结构指令 改变DOM结构
ElementRef 访问DOM元素
Renderer2 安全操作DOM
HostListener 监听宿主事件
HostBinding 绑定宿主属性
TemplateRef 模板引用
ViewContainerRef 视图容器

下一步:服务基础

最后更新:2026-03-26