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 bindingstyle 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');
  }
}
View Code

调用

<div class="card">
  <h1 appHighlightOnHover="red">Hello World</h1>
  <p [appHighlightOnHover]="'blue'">Lorem ipsum dolor sit amet consectetur adipisicing elit. Minus, minima.</p>
</div>
View Code

 效果

冷知识 – @Input 会导致 attribute 消失

两个知识点:

  1. lowercase

    HighlightOnHover 指令的 selector 是 '[appHighlightOnHover]'

    我们在 App Template 写的是 camelCase

    <h1 appHighlightOnHover="red">

    最终的 DOM attribute 变成了 apphighlightonhover="red" lowercase。

  2. 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 binddingstyle 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 几乎是一样的,几个点注意一下:

  1. 支持输入 string, array, object

  2. array 内不能有 null 和 undefined, empty string 则可以(会被 ignore)

  3. object 的属性值会被强转成 boolean,所以你要用 number 0 | 1 来表示 true false 也是可以的。

  4. object 不需要 immutable(class binding 则需要)

ngStyle

<h1 [ngStyle]="{ 'width.px' : 100, height: '100px', color: null }">Hello World</h1>

ngStyle 和 style binding 几乎是一样的, 几个点要注意一下

  1. 支持输入 object 而已

  2. object 不需要 immutable(style binding 则需要)

  3. 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 😊💻

 

posted @ 2023-06-19 20:21  兴杰  阅读(447)  评论(0编辑  收藏  举报