自定义指令 #
一、指令概述 #
自定义指令分为两种类型:
| 类型 | 说明 | 示例 |
|---|---|---|
| 属性指令 | 改变元素外观或行为 | 高亮、焦点、权限 |
| 结构指令 | 改变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