Angular复习笔记5-指令
在Angular中,指令是一个重要的概念,它作用在特定的DOM元素上,可以扩展这个元素的功能,为元素增加新的行为。本质上,组件可以被理解为一种带有视图的指令。组件继承自指令,是指令的一个子类,通常被用来构造UI控件。
指令的使用并不复杂,它与HTML元素属性的使用方式相似。不同的是,HTML语法标准为HTML元素预定义了特定的属性,浏览器遵循这一语法标准,实现了这些属性的内置行为。语法标准预定义的属性是有限的、不可扩展的,而Angular中的指令是可自定义的、可任意扩展的,这在一定程度上弥补了标准HTML元素属性功能的不足。
指令分类
在angular中指令分为三类:属性型指令,结构型指令和组件。
属性型指令
顾名思义,属性指令是以元素属性的形式来使用的指令。与HTML元素的内置属性不同,指令是Angular对HTML元素属性的扩展,浏览器本身不能识别这些指令,指令仅在Angular环境中才能被识别使用。属性指令通常被用来改变元素的外观和行为,如在第7章中介绍过的Angular内置指令NgStyle,它可以基于组件的状态来动态设置目标元素的样式。
结构型指令
结构指令可以用来改变DOM树的结构。结构指令可以根据模板表达式的值,增加或删除DOM元素,从而改变DOM的布局。结构指令与属性指令的使用方式相同,都是以元素属性的形式来使用的。两者的区别在于使用场景不同,属性指令用来改变元素的外观和行为,而结构指令用来改变DOM树的结构。
以Angular内置的结构指令NgIf为例,使用NgIf指令需要为指令绑定一个表达式,当表达式值为true时,该DOM元素及其子元素被添加至DOM中;当表达式值为false时,元素从DOM中被移除。示例代码如下:
上面的代码当condition的值为true时会显示p元素,反之p元素会从DOM中移除。
组件
组件继承自指令,它的代码结构与指令也是相似的,不同之处在于组件有一个自描述的模板,并且组件是用@Component来修饰,而指令需要一个宿主元素,用@Directive来描述。
组件与指令的部分生命周期钩子函数相同:
尽管组件与指令有相似的结构和一些相同的生命周期钩子方法,但是它们也有一些不同点。不同于属性指令和结构指令,组件不是以HTML元素属性的形式使用的,而是以自定义标签的形式使用的,原因在于组件带有模板。组件可作为对HTML元素的扩展,将自身的模板视图插入DOM中;而属性指令和结构指令是对HTML元素属性的扩展,其作用是扩展已有DOM元素的行为和样式,或者改变这些元素在DOM中的结构。
自定义属性型指令
一个属性指令需要一个控制器类,该控制器类使用@Directive装饰器来装饰。@Directive装饰器指定了用以标记指令所关联属性的选择器,控制器类则实现了指令所对应的特定行为。
首先需要从@angular/core中引入Directive和ElementRef。前者包含了修饰指令的装饰器@Directive,后者通过指令的构造函数传入,代表指令修饰的DOM元素宿主。
@Directive({ selector: '[appFirst]' }) export class FirstDirective implements OnInit { constructor(el: ElementRef) { } ngOnInit(): void { } }
上面的代码便是创建了一个指令,我们需要在@Directive中定义这个指令的选择器,选择器的规则遵循CSS选择器标准,例如上面的选择器是[appFirst],表示该指令会以属性的方式附着在元素上面。
Angular会为每一个匹配的DOM元素创建一个指令实例,同时将ElementRef作为参数注入到控制器构造函数。使用ElementRef服务,可以在代码中通过其nativeElement属性直接访问DOM元素,这样就可以通过DOM API设置元素的背景色:
@Directive({ selector: '[appFirst]' }) export class FirstDirective implements OnInit { constructor(private el: ElementRef) { } ngOnInit(): void { console.log(this.el); this.el.nativeElement.style.backgroundColor = 'red'; } }
为指令绑定输入
在上述的代码中指令为宿主设置的颜色是固定的,如果要想动态的变更颜色,有两个思路:
第一,设置一个@Input输入属性,并将这个属性定义为set函数:
@Directive({ selector: '[appFirst]' }) export class FirstDirective { @Input() set backgroundColor(color: string) { if (color) { this.el.nativeElement.style.backgroundColor = color; } } constructor(private el: ElementRef) { } }
第二,指令实现onChanges接口,在接口中处理逻辑:
@Directive({ selector: '[appFirst]' }) export class FirstDirective implements OnChanges { @Input() backgroundColor: string; ngOnChanges(changes: SimpleChanges): void { this.el.nativeElement.style.backgroundColor = this.backgroundColor; } constructor(private el: ElementRef) { } }
然后在宿主中使用这个指令:
<p appFirst [backgroundColor]="color" matLine>template</p>
在宿主中需要定义一个color变量并且改变这个color变量的值,这里就不演示了。
响应用户操作
可以使用@HostListener来响应用户发出的事件。@HostListener指向使用指令的DOM元素,使得DOM元素的事件与指令关联起来。@HostListener是另一种动态设置的方案,相比较前面那种@Input set输入属性的方式还有生命周期钩子的方式,这种方式是从响应用户发出的事件的角度执行的。
//.....其他代码.... @HostListener('click') onClick(){ //.....代码逻辑.... }
自定义结构型指令
不同于属性型指令,属性型指令会在构造函数中传入一个ElementRef来表示它的宿主DOM元素,而结构型指令需要在构造器中传入两个参数,一个是TempleteRef,另一个是ViewContainerRef.这是因为被结构型指令修饰的DOM元素会被angular转译成一个<ng-temeplate>元素:
<span *ngIf="condition">can you see it?</span>
上面的代码会被angular转译为:
<ng-temeplate [ngIf]="condition"> <span>can you see it?</span> </ng-template>
可以看出结构型指令会被转译成一个属性型的指令。
所以,结构型指令需要传入一个TemeplateRef的服务来访问到这个组件模板(被转译成的ng-template)。至于另一个ViewContainerRef,这个服务是可以从DOM中创建或者删除一个TemplateRef所代表的模板元素,要取决于对结构型指令求值的boolean值(因为给结构型指令绑定的表达式必须是一个boolean类型的表达式)。
下面代码简单定义了一个结构型的指令:
@Directive({ selector: '[appFirst]' }) export class FirstDirective { constructor(templ: TemplateRef<any>, container: ViewContainerRef) { } @Input() condition: boolean; }
看起来和属性型的指令差不多,不同之处在于构造函数传入的参数类型不同。使用上也和属性型指令差不多,需要注意的就是ViewContainerRef的两个方法,这两个方法都是用来操作指令中的TemplateRef所指向的模板的:
createEmbeddedView():需要传入一个TemplateRef类型的参数,表示根据TemplateRef创建一个内嵌的视图模板。
clear():将TemplateRef视图模板中删除。