Angular 指令组合 API—— hostDirectives

前言

有一种场景是给动态创建组件添加指令,当时可难坏了,因为 createComponent API 不支持绑定指令,所以我当时猜想是否可以在 @Component 注入或 @HostBindingDirective 绑定实现呢?找了很久,最终发现官方支持了这种,选择的是前者,Feature Issue 如下:

具体详情: github.com/angular/ang...

简介

hostDirectives 是以依赖注入( @Component 装饰器)的方式给宿主绑定指令,该特性于 Angular 15 支持。

基本使用:

typescript 复制代码
    @Component({
      selector: 'app-demo',
      template: '<div></div>',
      hostDirectives: [CdkDrag]
    })
    export class DemoComponent {}

等价于

html 复制代码
    <app-demo cdkDrag></app-demo>

有些类似 host 属性(或 @HostBinding('class') className = 'active' )和 <div class='active'></div> 的关系。

需要注意的点

  • Angular 会在编译时静态应用宿主指令。不能在运行时动态添加指令。
  • hostDirectives 中使用的指令必须是 standalone: true 。
  • Angular 会忽略 hostDirectives 属性中所应用的那些指令的 selector 。

还有个小区别是,hostDirectives 添加的指令不会给 dom 元素加上属性,不过这并不影响效果。

指令支持输入(属性)与输出(事件)

  1. 创建一个指令
typescript 复制代码
    import { Directive, EventEmitter, Input, OnInit, Output, ViewContainerRef } from '@angular/core';

    @Directive({
      selector: '[one]',
      exportAs: 'one',
      standalone: true
    })
    export class DirectiveOne implements OnInit {
      @Input() public name: string;

      @Output() nameChange = new EventEmitter();

      constructor(private viewContainer: ViewContainerRef) {}

      ngOnInit(): void {
        console.log('one input name: ' + this.name);
        this.viewContainer.element.nativeElement.style = 'color: red;';
        setTimeout(() => {
          this.nameChange.emit('one name output');
        }, 1000);
      }
    }
  1. 组件使用指令添加输入输出参数
typescript 复制代码
    @Component({
      selector: 'app-demo',
      standalone: true,
      imports: [DirectiveOne],
      hostDirectives: [
        {
          directive: DirectiveOne,
          inputs: ['name'],
          outputs: ['nameChange']
        }
      ],
      template: `脱我战时袍,著我旧时裳`,
    })
    export class DemoComponent {}
  1. 使用组件绑定输入属性和事件返回
typescript 复制代码
    import { Component, Input } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { bootstrapApplication } from '@angular/platform-browser';
    import { DemoComponent } from './demo.component';

    @Component({
      selector: 'my-app',
      imports: [CommonModule, DemoComponent],
      standalone: true,
      template: `
        <app-demo [name]="name" (nameChange)="change($event)"></app-demo>
      `,
    })
    export class App {
      @Input() name = 'Angular app';

      change(value) {
        console.log(value);
      }
    }

效果显示:

给输入/输出起别名

typescript 复制代码
    {
          directive: DirectiveOne,
          inputs: ['name: Name'],
          outputs: ['nameChange: NameChange']
    }

<app-demo [Name]="name" (NameChange)="change($event)"></app-demo>

指令中引入另一个指令

方便理解,我把这种叫做指令嵌套或指令继承

  1. 指令 DirectiveTwo
typescript 复制代码
    import { Directive } from '@angular/core';
    import { DirectiveOne } from './directive-one';

    @Directive({
      selector: '[two]',
      standalone: true,
      exportAs: 'two',
      hostDirectives: [DirectiveOne]
    })
    export class DirectiveTwo {}
  1. 组件使用指令
typescript 复制代码
    import { Component, Input } from '@angular/core';
    import { DirectiveTwo } from './directive-two';

    @Component({
      selector: 'app-demo',
      standalone: true,
      imports: [DirectiveTwo],
      hostDirectives: [DirectiveTwo],
      template: `脱我战时袍,著我旧时裳`,
    })
    export class DemoComponent { }

指令嵌套的执行顺序

以上述为例,大体的执行顺序为: DirectiveOne -> DirectiveTwo

加上每个指令生命钩子后的顺序为: DirectiveOne constructor(初始化)-> DirectiveTwo constructor(初始化)-> DirectiveOne ngOnInit -> DirectiveTwo ngOnInit ...... 等等, 这个顺序意味着后续的指令可以修改前面指令对宿主元素的操作

示例如下:

  1. DirectiveOne
typescript 复制代码
import { Directive, EventEmitter, Input, OnInit, Output, ViewContainerRef } from '@angular/core';

    @Directive({
      selector: '[one]',
      exportAs: 'one',
      standalone: true,
    })
    export class DirectiveOne implements OnInit {
      @Input() public name: string;
      @Output() nameChange = new EventEmitter();
      constructor(private viewContainer: ViewContainerRef) {
        console.log('one constructor');
      }
      ngOnInit(): void {
        console.log('one ngOnInit');
        // console.log('one input name: ' + this.name);
        this.viewContainer.element.nativeElement.style = 'color: red;';
        setTimeout(() => {
          this.nameChange.emit('one output');
        }, 1000);
      }
    }
  1. DirectiveTwo
typescript 复制代码
import { Directive, EventEmitter, Input, OnInit, Output, ViewContainerRef } from '@angular/core';
    import { DirectiveOne } from './directive-one';

    @Directive({
      selector: '[two]',
      standalone: true,
      exportAs: 'two',
      hostDirectives: [
        {
          directive: DirectiveOne,
          inputs: ['name'],
          outputs: ['nameChange']
        }
      ]
    })
    export class DirectiveTwo implements OnInit {
      @Input() public name = '';
      @Output() nameChange = new EventEmitter();
      constructor(private viewContainer: ViewContainerRef) {
        console.log('two constructor');
      }
      ngOnInit(): void {
        console.log('two ngOnInit');
        // 覆盖宿主元素
        this.viewContainer.element.nativeElement.innerHTML += ';<span>当窗理云鬓,对镜帖花黄</span>。';
        this.viewContainer.element.nativeElement.style = 'color: red; background-color: yellow;';
      }
    }
  1. 组件使用指令
typescript 复制代码
    import { Component } from '@angular/core';
    import { DirectiveTwo } from './directive-two';

    @Component({
      selector: 'app-demo',
      standalone: true,
      imports: [DirectiveTwo],
      hostDirectives: [
        {
          directive: DirectiveTwo,
          inputs: ['name'],
          outputs: ['nameChange']
        }
      ]
      template: `脱我战时袍,著我旧时裳`
    })
    export class DemoComponent {
      constructor() {
        console.log('demo constructor');
      }

      ngOnInit() {
        console.log('demo ngOnInit');
      }
    }
  1. 使用组件
typescript 复制代码
    @Component({
      selector: 'my-app',
      imports: [CommonModule, DemoComponent],
      standalone: true,
      template: `
        <app-demo [name]="name" (nameChange)="change($event)"></app-demo>
      `,
    })
    export class App {
      @Input() name = 'Angular app';
    }

效果展示:

最终的效果可以发现 DirectiveTwo 在 DirectiveOne 的基础上修改了宿主元素。

向动态创建的组件添加指令

场景:使用官方 cdk 的拖拽组件实现将左侧组件拖拽至可添加区域(中间部分),并且中间的元素可拖拽排序,所以中间部分也必须是一个 cdkDragList,复制出的组件必须包含指令 cdkDrag。

  1. 动态创建组件:

2. 给组件添加 cdkDrag 指令

dom 元素

完整代码

angular-directive-compose-api - StackBlitz

参考

相关推荐
纯爱掌门人18 分钟前
鸿蒙端云一体化云存储实战:手把手教你玩转文件上传下载
前端·harmonyos
非凡ghost20 分钟前
图吧工具箱-电脑硬件圈的“瑞士军刀”
前端·javascript·后端
非凡ghost21 分钟前
Xrecode3(多功能音频转换工具)
前端·javascript·后端
橙某人23 分钟前
飞书多维表格插件:进一步封装,提升开发效率!🚀
前端·javascript
他们叫我秃子34 分钟前
从 0 到 1,我用小程序 + 云开发打造了一个“记忆瓶子”,记录那些重要的日子!
前端·微信小程序·小程序·云开发
非凡ghost35 分钟前
Subtitle Edit(字幕编辑软件) 中文绿色版
前端·javascript·后端
扎瓦斯柯瑞迫39 分钟前
cursor: 10分钟魔改环境、优雅获取Token
前端·javascript·后端
王六岁39 分钟前
🐍 前端开发 0 基础学 Python 入门指南:条件语句篇
前端·python
San3042 分钟前
CSS3 星球大战:用前端技术打造震撼的3D动画效果
前端·css·html
用户120391129472642 分钟前
从零构建一个HTML5敲击乐Web应用:前端开发最佳实践指南
前端