Angular 18+ 高级教程 – Component 组件 の Attribute Directives 属性型指令
介绍
指令就是没有模板的组件。除了模板其它的都有,比如 selector、inject、@Input、lifecycle 等等。
那既然都有完整的组件了,为什么还搞一个少掉模板的指令呢?
很简单啊,因为就是不需要模板丫。没道理不需要还硬硬要放吧。
组件是 JS、CSS、HTML 的封装,但很多时候,我们可能只想要封装 JS 和 CSS。
这个时候就可以使用指令啦。
Attribute Directives & Structural Directives
指令有两大类, 一个叫 Attribute Directives,另一个叫 Structural Directives。
Structural Directives 属于动态 Element 的范畴,比较复杂,而且需要一些 ng-template,ng-container 的预备知识。所以这篇不会讲解。我们留给下一篇。
这篇我们主要讲解 Attribute Directives 就好了。
Attribute Directives
我们直接从例子中学习呗。
我想封装一个 JS + CSS 的功能,效果是当某个 element 被 hover 时,它的字会变成红色。
这个 element 可以是 h1-h6、p、li、a 等等。如果使用组件来实现,那感觉就很怪,你都不知道模板要写啥。
来看看指令呗
ng g d highlight-on-hover
用 CLI 创建一个指令,d 是 directive 的缩写。
@Directive({ selector: '[appHighlightOnHover]', standalone: true, }) export class HighlightOnHoverDirective {}
它和组件一样,都是一个 class,selector 通常是 attribute (组件则通常是 tag。 对,只是通常而已,其实指令也可以是 tag,组件也可以是 attribute)。
另外提一个冷知识,attribute 'appHighlightOnHover' 最后在 element 上会变成 lowercase <div apphighlightonhover>。
通过 inject 获取到当前的 element。
private element: HTMLElement = inject(ElementRef).nativeElement;
这样我们就可以对 element 做操作了。
加上两个核心方法, setColor 和 clearColor。
setColor() { this.element.style.color = 'red'; } clearColor() { this.element.style.removeProperty('color'); }
指令没有模板,也就不存在 MVVM 的概念。在这里直接操作 DOM 是正确的。
补上一个 transition 过度 (让体验好一下)
constructor() { this.element.style.transition = 'color 0.4s'; }
注意:如何我们项目需要做服务端渲染 (Server-side rendering),那就不可以在 constructor 阶段直接操作 DOM。
必须使用 Renderer 做渲染。
constructor() { const renderer = inject(Renderer2); // constructor 阶段需要使用 Renderer2 renderer.setStyle(this.element, 'transition', 'color 0.4s'); }
完整的 class
export class HighlightOnHoverDirective { private element: HTMLElement = inject(ElementRef).nativeElement; constructor() { const renderer = inject(Renderer2); // constructor 阶段需要使用 Renderer2 renderer.setStyle(this.element, 'transition', 'color 0.4s'); } setColor(): void { // 这里不需要使用 Renderer2,可以直接操作 DOM this.element.style.color = 'red'; } clearColor(): void { // 这里不需要使用 Renderer2,可以直接操作 DOM this.element.style.removeProperty('color'); } }
最后通过 metadata 加上 host listening
@Directive({ selector: '[appHighlightOnHover]', standalone: true, host: { '(mouseenter)': 'setColor()', '(mouseleave)': 'clearColor()', }, })
使用它
<div class="card"> <h1 appHighlightOnHover>Hello World</h1> <p appHighlightOnHover>Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus, minima.</p> </div>
在任何 element 添加上 appHighlightOnHover 就可以了。记得要添加 @Component.imports
效果
Host binding and with class / style binding
上面例子中,指令使用到了 host listening 技术。
若我们想使用 host binding 自然也没问题,下面给一个 class binding 和 style binding 的例子
host: { // 这是 host binding style: 'color: red; z-index: 1', // 注:z-index 是 kebab-case // 这是 host binding + style binding '[style.backgroundColor]': `'blue'`, // 注:backgroundColor 是 lowerCase, 'blue' 有 quote // 这是 host binding class: 'my-class, my-class2', // 这是 host binding + class binding '[class.my-class-3]': 'true', },
效果
指令 @Input
指令和组件一样,可以有 @Input @Output,这里给一个 @Input 的例子。
export class HighlightOnHoverDirective { private element: HTMLElement = inject(ElementRef).nativeElement; @Input({ alias: 'appHighlightOnHover', required: true }) color!: string; setColor() { this.element.style.color = this.color; } clearColor() { this.element.style.removeProperty('color'); } constructor() { const renderer = inject(Renderer2); renderer.setStyle(this.element, 'transition', 'color 0.4s'); } }
调用
<div class="card"> <h1 appHighlightOnHover="red">Hello World</h1> <p [appHighlightOnHover]="'blue'">Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus, minima.</p> </div>
效果
冷知识 – @Input 会导致 attribute 消失
两个知识点:
-
lowercase
HighlightOnHover 指令的 selector 是 '[appHighlightOnHover]'
我们在 App Template 写的是 camelCase
<h1 appHighlightOnHover="red">
最终的 DOM attribute 变成了 apphighlightonhover="red" lowercase。
-
attribute 消失了
上图的 <p> 完全没有 apphighlightonhover="blue",因为在 App Template 写的是 binding syntax
p [appHighlightOnHover]="'blue'">
这会导致最终的 DOM 连 attribute 都没有。(when using binding syntax + @Input alias same name as selector 就会出现这种现象)
如果我们希望它一定有 attrubute 的话,可以透过 host binding attribute 补上去
效果
综上两点,如果想用指令 attribute 作为 CSS selector 要特别留意哦。
Angular Build-in Attribute Directives (ngClass, ngStyle)
我们之前学过 class bindding 和 style binding,当时有说过,它们不适合处理 multiple 的场景。multiple 场景比较适合用 ngClass 和 ngStyle 属性型指令。
ngClass
<h1 [ngClass]="'abc efg'">Hello World</h1> <h1 [ngClass]="['abc', 'efg', '']">Hello World</h1> <!--null, undefined is not allowed--> <h1 [ngClass]="{ abc: true, efg: false, xyz: 1 }">Hello World</h1> <!--null, undefined is not allowed-->
ngClass 和 class binding 几乎是一样的,几个点注意一下:
-
支持输入 string, array, object
-
array 内不能有 null 和 undefined, empty string 则可以(会被 ignore)
-
object 的属性值会被强转成 boolean,所以你要用 number 0 | 1 来表示 true false 也是可以的。
-
object 不需要 immutable(class binding 则需要)
ngStyle
<h1 [ngStyle]="{ 'width.px' : 100, height: '100px', color: null }">Hello World</h1>
ngStyle 和 style binding 几乎是一样的, 几个点要注意一下
-
支持输入 object 而已
-
object 不需要 immutable(style binding 则需要)
-
object 属性 支持 suffix unit(style binding 传入 object 是不支持的)
属性型指令 host binding with ngClass / ngStyle?
host: { class: 'a, b, c', // this work '[ngClass]': '{ x: true, y: false, z: 1 }', // this not work },
指令想 host binding 其它指令需要使用 Directive composition API (下一 part 会教),然而它也有一些 limitation,像 ngClass / ngStyle 是无法被 host binding 的。
Directive composition API
使用指令的方法是把指令写到 element 上。那如果我有一个组件,我能把指令写到 host 上吗?
我们之前学过 Host Binding and Listening,想当然的会认为肯定没有问题呀。
但 Angular 其实一直到 v15.0 才实现了这个功能。
<app-my-h1 appHoverColor></app-my-h1>
我们希望把 appHoverColor 封装到 app-my-h1 里。
@Component({ selector: 'app-my-h1', standalone: true, imports: [CommonModule, HoverColorDirective], templateUrl: './my-h1.component.html', styleUrls: ['./my-h1.component.scss'], hostDirectives: [ { directive: HoverColorDirective, }, ], }) export class MyH1Component {}
通过组件的 metadata hostDirectives 属性做配置。
注:它不是用 HostBinding 哦。HostBinding 只能用于原生 HTML 属性,不可以用于 @Input 也不可以用于指令。
提醒1:hostDirectives 一定要是 Standalone Component 哦。
提醒2:hostDirectives 输出的指令是不带 selector 的,比如说 HoverColorDirective 的 selector 是 [appHorverColor],MyH1 组件用 hostDirectives 声明了 HoverColorDirective,最终 <app-my-h1> 并不会出现 appHorverColor attribute。
指令 @Input
如果指令需要 input 那就有点麻烦了。
export class HighlightOnHoverDirective { @Input({ required: true }) color!: string; }
指令需要一个 color input。
首先,Angular 目前只提供了一条路来输入 input。
我们需要在 hostDirectives re-expose 这个 input。
hostDirectives: [ { directive: HighlightOnHoverDirective, inputs: ['color'], // re-expose input // inputs: ['color: my-color'], 通过分号还可以换一个属性名 }, ],
最后在使用组件时传入
<app-my-h1 color="red"></app-my-h1>
re-expose Input 的问题
显然强制 re-expose 是不逻辑的,难道我们不可以在组件内封装指令的 input 逻辑吗?
目前,Angular 没有给出其它路,虽然我们确实有方法可以做到这一点。
export class MyH1Component { constructor() { inject(HighlightOnHoverDirective).color = 'red'; // 如果是 input Signal 会更麻烦一点 const highlightOnHoverDirective= inject(HighlightOnHoverDirective); const colorInputSignalNode = highlightOnHoverDirective.color[SIGNAL]; colorInputSignalNode.applyValueToInputSignal(colorInputSignalNode, 'red'); } }
像上面这样就可以在不 re-expose input 的情况下,set value to input 了。
但...通常 Angular 没有给出一条路,是因为他们没有想到...或者认为没有那么重要。所以很容易出 bug。
比如...指令的 input 如果设置了 required,那会报错。参考: Issue: required inputs shouldn't always have to be exposed by composed directive。
另外,上面这种方式严格的讲是叫 "给组件实例属性赋值",而不是 "给组件 set @Input value",这两者区别还是挺大的,比如 @Input 的 transform 会失效,不会触发 ngOnChanges 等等问题。
总之,近期内 Angular Team 视乎没有意愿去改善这个问题😱
为此我还特意搜了一下 Angular Material 源码。结果发现里头完全没有使用 hostDirectives 功能...难怪他们没有意愿改进...
目录
上一篇 Angular 18+ 高级教程 – Component 组件 の Template Binding Syntax
下一篇 Angular 18+ 高级教程 – Component 组件 の Pipe 管道
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻