[Angular 基础] - 自定义指令,深入学习 directive

[Angular 基础] - 自定义指令,深入学习 directive

这篇笔记的前置笔记为 [Angular 基础] - 数据绑定(databinding),对 Angular 的 directives 不是很了解的可以先过一下这篇笔记

后面也会拓展一下项目,所以感兴趣的也可以补一下文后对应的项目:

  • 第一个 Angular 项目 - 静态页面
  • 第一个 Angular 项目 - 动态页面

创建新 directive

directive 的创建方式和 component 类似,这里选择用指令生成:

❯ ng g d directives/test --skip-tests
CREATE src/app/directives/test.directive.ts (137 bytes)
UPDATE src/app/app.module.ts (757 bytes)

运行这个指令就会在 src/app/directives 下创建一个新的 directive:

在这里插入图片描述

目前项目结构如下,这里 V 和 VM 层暂时不用去管,directive 会一个个过一遍

⚠️:如果手动生成 directive,同样需要在 app.module.ts 中导入对应的 directive:

@NgModule({
  declarations: [
    AppComponent,
    BasicHighlightDirective,
    BetterHighlightDirective,
  ],
  imports: [BrowserModule, FormsModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

实现一个 attribute directive

一个空白的 directive 结构如下:

import { Directive } from '@angular/core';

@Directive({
  selector: '[appBasicHighlight]',
})
export class BasicHighlightDirective {
  constructor() {}
}

首先分析一下 directive 的结构,这里的 @Directive@Component 一样都是装饰器,不过这里的使用比较简单,只是放入了一个 selector

selector 中的内容为绑定对应的 HTML Template 中的 attribute,也就是说 HTML template:

  • ✅ 使用 appBasicHighlight,就能够绑定对应的 directive
  • ❌ 不使用 [appBasicHighlight] 去进行属性绑定

这时候修改 app 的 VM 层:

<p appBasicHighlight>Style me with basic directive</p>
`

简单的 attribute directive

directive 的构造函数是比较重要的,它总共会提供 3 个参数用来操控对应的 DOM

使用 ElementRef

这里的 ElementRef 是对当前绑定指令的 HTML 元素的引用值,这里也就是 p 标签中的内容

通过直接修改 ElementRef 也是一种可以直接修改 DOM 元素的方式,使用方法如下:

@Directive({
  selector: '[appBasicHighlight]',
})
export class BasicHighlightDirective implements OnInit {
  constructor(private elementRef: ElementRef) {}

  ngOnInit() {
    console.log(this.elementRef);
    this.elementRef.nativeElement.style.backgroundColor = 'pink';
  }
}

效果如下:

在这里插入图片描述

这里简述一下修改:

  • implements OnInit 这一段算是补充吧

    尽管说 TS 的 implements 执行力比较差,不过我看了下官方文档都有用,就稍微标准化一下好了

  • ngOnInit 中执行对于样式的修改

    本案例使用 ngOnInit 或者直接在构造函数里修改样式其实没有什么特别大的区别,不过对于其他的情况,例如 HTML 中的内容是动态生成的情况下,直接在构造函数里就会造成内容的缺失。

通过这种方式就能够创建一个 attribute directive 了,不过直接使用 elementRef 并不是一个推荐的做法,下面会创建另一个 attribute directive,并使用推荐的方式去修改属性

复杂一些的 attribute directive

这里新建一个 directive,并将其命名为 better-highlight.directive,同时在 app 的 V 层新建一个 p 标签,并添加对应的 attribute directive:

<p appBetterHighlight>Style me with better directive</p>
renderer
import { Directive } from '@angular/core';

@Directive({
  selector: '[appBetterHighlight]',
})
export class BetterHighlightDirective {
  constructor(private renderer: Renderer2) {}
}

这里要修改样式的方法是通过这个 renderer 去实现的

renderer: Renderer2 是 Angular 对 DOM 操作的一个 service 封装,其主要的优点在于提供统一的 API 使得其在浏览器坏境、SSR 环境以及 web worker 中都会有同样的表现。另外它也会对一些 HTML 元素进行清理,这样可以更觉有效的防止 XSS 攻击。

对于同样修改样式,这里依旧在 ngOnInit 中实现,实现的方式是通过调用 setStyle 进行:

@Directive({
  selector: '[appBetterHighlight]',
})
export class BetterHighlightDirective implements OnInit {
  // @Input
  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

  ngOnInit() {
    this.renderer.setStyle(
      this.elementRef.nativeElement,
      'background-color',
      'lightblue'
    );
  }
}

显示效果如下:

在这里插入图片描述

@HostListener

@HostListener 是 Angular 提供的,对当前元素所提供的事件绑定的一个装饰器,其语法如下:

@HostListener('event_name', ['$event'])
methodName(event: EventType): void {}

其中 :

  • event_name 为事件名称,如 click, mouseenter

  • $event 为对应的事件

    事件前添加 $ 算是 Angular 约定俗成的一种规范了

  • methodName,如其名,函数名

  • EventType,事件类型,可以不传,TS 用来做规范的

这里用 mouseentermouseleave 为例,对背景颜色进行修改,修改后代码如下:

@Directive({
  selector: '[appBetterHighlight]',
})
export class BetterHighlightDirective implements OnInit {
  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

  ngOnInit() {
    this.renderer.setStyle(
      this.elementRef.nativeElement,
      'background-color',
      'lightblue'
    );
  }

  @HostListener('mouseenter', ['$event']) mouseover(eventData: MouseEvent) {
    console.log(eventData);

    this.renderer.setStyle(
      this.elementRef.nativeElement,
      'background-color',
      'lightgreen'
    );
  }

  @HostListener('mouseleave') mouseleave(eventData: Event) {
    console.log(eventData);

    this.renderer.setStyle(
      this.elementRef.nativeElement,
      'background-color',
      'lightblue'
    );
  }
}

效果如下:

在这里插入图片描述

这里主要注意的是这么几个点:

  • 如果不传递 ['$event'],那么函数也不会自动监听到对应的事件

    这也是为什么 mouseenter 的时候能抓到 $event,但是 mouseleave 的时候抓不到的原因

  • Event 只是类型检查

    不是说没用,相反,如果确定类型的话,那么 TS 将会提供更好的类型检查和 intelligence 提示

    但是如果要写比较 generic 的方案,可能还是直接用 Event 比较好

💡:我添加了 CSS transition,让背景色过渡的稍微自然点,不过这个不是什么重点

@HostBinding

这个时候看到组件内出现了很多的重复代码:

this.renderer.setStyle(
  this.elementRef.nativeElement,
  'background-color',
  `${color}`
);

可以看到,这里唯一产生变化的只有需要被修改的颜色。

要解决这个问题,可以使用 Angular 提供的 @HostBinding 装饰器,它的语法为:

@HostBinding('property') propertyName: Type = value;

其中:

  • property 为想要绑定的元素属性

    这个案例中就是 style.backgroundColor

  • propertyName 为变量名

  • Type 为类型

    本案例为 string,其他的案例可能会出现 number, boolean,如果是可选项的话也可以为 undefined

  • value

使用如下:

export class BetterHighlightDirective implements OnInit {
  @HostBinding('style.backgroundColor') backgroundColor: string = 'lightblue';

  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

  ngOnInit() {
    // 已经有了默认值,这下面的代码也可以注释掉了
    // this.backgroundColor = 'lightblue';
  }

  @HostListener('mouseenter', ['$event']) mouseover(eventData: MouseEvent) {
    console.log(eventData);

    this.backgroundColor = 'lightgreen';
  }

  @HostListener('mouseleave') mouseleave(eventData: Event) {
    console.log(eventData);

    this.backgroundColor = 'lightblue';
  }
}

最后展示的效果依旧是一样的

动态添加属性

这个时候可以注意到,现在唯一要修改的地方就是颜色,这个情况也可以使用变量去存储这个修改的颜色,同时前面可以添加 @Input,这样可以让父元素动态重写颜色:

export class BetterHighlightDirective implements OnInit {
  @Input() defaultColor: string = 'lightblue';
  @Input() highlightColor: string = 'lightgreen';
  @HostBinding('style.backgroundColor') backgroundColor: string =
    this.defaultColor;

  constructor(private elementRef: ElementRef, private renderer: Renderer2) {}

  @HostListener('mouseenter', ['$event']) mouseover(eventData: MouseEvent) {
    console.log(eventData);
    this.backgroundColor = this.highlightColor;
  }

  @HostListener('mouseleave') mouseleave(eventData: Event) {
    console.log(eventData);

    this.backgroundColor = this.defaultColor;
  }
}

在不重写默认颜色时,效果是一样的。

但是父元素也可以选择重写默认值:

<p
  appBetterHighlight
  class="bg-transition"
  [defaultColor]="'lightyellow'"
  highlightColor="violet"
>
  Style me with better directive
</p>

效果如下:

在这里插入图片描述

这里又有两个点需要注意的:

  • highlightColor="violet"

    这是一个特殊的语法缩写,本质上还是一个 property binding,而不是 HTML 所有的原生属性

    我这里特地用了两种写法,只是为了添加一下 note。为了更好的阅读性和理解,还是推荐使用 [customPropertyName]="'value'" 的写法

  • 背景颜色默认为蓝色

    这就是前面提到的 ngOnInit 的作用,这个情况下 Angular 的组件需要经历一个初始化的状态,在这个初始化的状态,它会绑定对应的属性——包括来自外部的属性

    这一段代码里我特地把 ngOnInit 注释掉了,没有了这个初始化的状态,那么当前组件依旧接受默认值,也就是 lightblue,把 ngOnInit 加回去,并添加对应的修改:

    ngOnInit() {
      this.backgroundColor = this.defaultColor;
    }
    

    才能将默认的背景色重写为父元素传进来的 lightyellow

实现一个 structural directive

官方文档有一个实现了 unless 的 structural directive,也可以参考一下,我这里就写一个 loading spinner 了。

实现如下:

@Directive({
  selector: '[appLoading]',
})
export class LoadingDirective {
  private loadingSpinner: HTMLElement;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
    private renderer: Renderer2,
    private el: ElementRef
  ) {
    this.loadingSpinner = this.renderer.createElement('div');
    this.renderer.addClass(this.loadingSpinner, 'spinner');
  }

  @Input() set appLoading(isLoading: boolean) {
    this.viewContainer.clear();
    if (isLoading) {
      this.renderer.appendChild(
        this.el.nativeElement.parentElement,
        this.loadingSpinner
      );
    } else {
      this.renderer.removeChild(
        this.el.nativeElement.parentElement,
        this.loadingSpinner
      );
      this.viewContainer.createEmbeddedView(this.templateRef);
    }
  }
}

app 的 V 层:

<div *appLoading="isLoading">
  some random syntax showing only when loading is false
</div>

VM 层修改变量,以及 CSS 我就不贴代码了

效果如下:

在这里插入图片描述

下面进入实现的分析部分

TemplateRef

即绑定 structural directive 的元素,在这个情况下,就是:

<div *appLoading="isLoading">
  some random syntax showing only when loading is false
</div>

简单的理解就是,当满足特定条件时,这里需要渲染的内容

ViewContainerRef

ViewContainerRef 就是管理渲染内容的容器

Angular 没有 virtual DOM,但是又不想直接暴露 DOM 进行操作,因此就像 ElementRef 一样,它对将整个 视图(view) 进行了一个抽象,创建了 ViewContainerRef 以方便管理与 directive 绑定的,整个 DOM 的 view/template

setter

set 是一个 JavaScript 的语法糖,这也是 ES6 后出现的语法,与 Angular 无关。

这里 Angular 动态的将其 setter 和 @Input 进行绑定,并且提供一个更加直观且简洁明了的方式对当前与 @Input 绑定的值进行变化管理。如果不使用 setter 的话,也可以在对应的 ngOnChangesngOnInit 中监听值的变化,并且进行对应的操作。

整个 setter 中做的操作就分为两步:

  1. isLoading = true

    这个情况下需要渲染一个 loading spinner——这在构造函数中就已经创建好了,并且使用 renderer 去进行渲染

    当前的 nativeElement 指向的是 <div *appLoading="isLoading"></div> 这个具体的元素

    因此这里的操作就是在 nativeElement 的父元素下,新增一个 loading spinner

  2. isLoading = false

    这个情况下 loading spinner 被移除,原本的 template view 被渲染

💡:至于选用 renderer 就是因为在 attribute directive 部分已经讲过了,而且实现起来比较方便。我找了一下不用 renderer 渲染的方式,需要用到 ComponentFactoryResolver,这个暂时就还没学上,等之后学上了再说

structural directive 幕后

如果看了官方文档就会知道,<div *ngIf="hero" class="name">{{hero.name}}</div> 这样的语法是一个缩写,本质上它的实现方法如下:

<ng-template [ngIf]="hero">
  <div class="name">{{hero.name}}</div>
</ng-template>

随后就像上面写过的 structural directive 的实现一样,ngIf 通过属性绑定被 @Input 监听到,structural component 再根据业务逻辑进行制定渲染。

ng-template 本身也是一个 view 的 placeholder,它是不会被渲染的

下面是官方文档关于 ngFor 的实现:

<div
  *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById"
  [class.odd]="odd"
>
  ({{i}}) {{hero.name}}
</div>

<ng-template
  ngFor
  let-hero
  [ngForOf]="heroes"
  let-i="index"
  let-odd="odd"
  [ngForTrackBy]="trackById"
>
  <div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>

也就是鉴于这样的转化,所以 structural directive 的语法一定是 *[directive_name]="[variable]" 这样的实现

第一个 Angular 项目——实现下拉框

项目里面没导入对应的 js 文件,所以现在 bootstrap 的 dropdown 是没办法被触发的。这里就是用 directive 去解决这个问题

新建 directive

❯ ng generate directive directives/dropdown --skip-tests
CREATE src/app/directives/dropdown.directive.ts (145 bytes)
UPDATE src/app/app.module.ts (1200 bytes)

实现 dropdown directive

这里需要了解一下 bootstrap 的 dropdown 是怎么被展开的——实际上是通过 open 这样一个 class 去实现的。因此,当 class 为 btn-group open 时,下拉框时展开的,当 class 为 btn-group 时,下拉框时关闭的。

所以这里需要实现一个 attribute directive,去管理 class 即可

最初的实现方法为:

export class DropdownDirective {
  constructor(private el: ElementRef, private renderer: Renderer2) {}

  @HostListener('click')
  onClick() {
    if (this.el.nativeElement.classList.contains('open')) {
      this.renderer.removeClass(this.el.nativeElement, 'open');
    } else {
      this.renderer.addClass(this.el.nativeElement, 'open');
    }
  }
}

不过一个简化的方法是使用 @HostBinding 去进行操作:

export class DropdownDirective {
  @HostBinding('class.open') isOpen = false;

  @HostListener('click')
  onClick() {
    this.isOpen = !this.isOpen;
  }
}

二者实现的效果是一样的:

在这里插入图片描述


补充一下点击 HTML 任何地方关闭 dropdown 的实现:

export class DropdownDirective {
  @HostBinding('class.open') isOpen = false;

  @HostListener('document:click', ['$event'])
  onClick(evemt: MouseEvent) {
    this.isOpen = this.el.nativeElement.contains(evemt.target)
      ? !this.isOpen
      : false;
  }

  constructor(private el: ElementRef) {}
}

在这里插入图片描述

reference

没有特殊标注的都是来自官方文档的内容

  • Structural directives
  • TemplateRef
  • ViewContainerRef

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/399229.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

钉钉小程序 访问ip不在白名单之中

钉钉小程序 访问ip不在白名单之中 problem 钉钉官方自带免登陆小程序 后端接口报错 {"errcode":60020,"errmsg":"访问ip不在白名单之中&#xff0c;请参考FAQ&#xff1a;https://open.dingtalk.com/document/org-faq/app-faq,request ip175.2.2.52…

软件测试工程师经典面试题

软件测试工程师&#xff0c;和开发工程师相比起来&#xff0c;虽然前期可能不会太深&#xff0c;但是涉及的面还是比较广的。前期面试实习生或者一年左右的岗位&#xff0c;问的也主要是一些基础性的问题比较多。涉及的知识主要有MySQL数据库的使用、Linux操作系统的使用、软件…

MyBatis3源码深度解析(一)搭建MyBatis源码环境

文章目录 前言内容概要 第1章 搭建MyBatis源码环境1.1 MyBatis3简介1.2 下载源码并导入IDE1.3 HSQLDB数据库&#xff08;1&#xff09;准备sql脚本&#xff08;2&#xff09;引入hsqldb依赖&#xff08;3&#xff09;编写单元测试&#xff08;4&#xff09;执行单元测试 1.4 小…

线阵相机参数介绍之轴编码器控制

1.1 功能介绍 编码器是将检测对象的运动与相机拍摄取图相匹配的设备&#xff0c;也即检测对象运动一定距离&#xff0c;相机就拍摄一定行高的图像。 编码器会将检测对象的实际位移转换为固定数量电信号。例如&#xff1a;编码器的精度是2000p/r,该参数的含义是编码器每转一圈输…

5款超好用、高颜值的Git可视化工具

Git&#xff0c;作为一款强大的分布式版本控制系统&#xff0c;为代码协作与版本追踪提供了坚实的基础。然而&#xff0c;对于不熟悉命令行操作的新手或寻求更直观体验的开发者来说&#xff0c;Git的可视化工具成为了不可或缺的得力助手。本文将分享五款超好用、高颜值的Git可视…

【微服务生态】Docker

文章目录 一、基础篇1. 简介2. 下载与安装3. 常用命令3.1 帮助启动类3.2 镜像命令3.3 容器命令 4. Docker 容器数据券5. Docker 镜像5.1 commit 生成镜像5.2 Docker Registry5.3 发布镜像 6. Docker 常规安装软件 二、高级篇1. Dockerfile1.1 概述1.2 基础知识1.3 Dockerfile常…

Zabbix 远程监控主机

目录 1、安装 Zabbix 安装客户端 服务端测试通讯 Web页面添加主机 2、监控 Nginx 自定义脚本监控 Nginx web配置台 3、监控 MySQL 配置模版文件 配置Web界面 1、安装 Zabbix node-12 作为zabbix的被监控端&#xff0c;提供mysql服务器&#xff0c;配置zabbix监控node…

汽车电子论文学习--电动汽车电机驱动系统动力学特性分析

关键重点&#xff1a; 1. 汽车的低速转矩存在最大限制&#xff0c;受附着力限制&#xff0c;因路面不同而变化。 2. 起步加速至规定转速的时间可以计算得到&#xff1a; 3. 电机额定功率的计算方式&#xff1a; 可以采取最高设计车速90%或120km/h匀速行驶的功率作为电机额定功…

机器学习基础(三)监督学习的进阶探索

导语&#xff1a;上一节我们深入地探讨监督学习和非监督学习的知识&#xff0c;重点关注它们的理论基础、常用算法及实际应用场景&#xff0c;详情可见&#xff1a; 机器学习基础&#xff08;二&#xff09;监督与非监督学习-CSDN博客文章浏览阅读769次&#xff0c;点赞15次&a…

【AI工具分享】Retrovision一个图片转3D模型在线工具

Retrovision:图像到3D模型的快速转换工具 一、工具简介 Retrovision是一款前沿的web应用程序,致力于将传统的2D图像快速、简便地转换为高质量的3D模型。其核心功能围绕视差图处理与3D网格生成,为3D内容创作者提供了一个全新的创作平台。 二、核心功能 视差图支持:Retrov…

maven打包scala程序

pom文件打包scala程序 <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&g…

快速上手Spring Boot整合,开发出优雅可靠的Web应用!

SpringBoot 1&#xff0c;SpringBoot简介1.1 SpringBoot快速入门1.1.1 开发步骤1.1.1.1 创建新模块1.1.1.2 创建 Controller1.1.1.3 启动服务器1.1.1.4 进行测试 1.1.2 对比1.1.3 官网构建工程1.1.3.1 进入SpringBoot官网1.1.3.2 选择依赖1.1.3.3 生成工程 1.1.4 SpringBoot工程…

前端进度条组件NProgress

nprogress 安装 npm install --save nprogress使用 import NProgress from nprogress // 引入nprogress插件 import nprogress/nprogress.css // 这个nprogress样式必须引入// axios请求拦截器 axios.interceptors.request.use(config > {NProgress.start() // 设置加载进…

CSS三大定位方式(浮动、定位、弹性盒)详细解析

CSS三大定位方式 前言&#xff1a;作为一名前端开发&#xff0c;已经工作2年了。由于自己是半路出家&#xff0c;从嵌入式方向转到前端开发&#xff0c;都是边百度边开发&#xff0c;很多基础都不了解&#xff0c;只要解决问题就好&#xff0c;但是近来为了让自己知识体系化&a…

Nginx 反向代理配置

Nginx就不废话了&#xff0c;web服务器。 最近在备案一个域名&#xff0c;想要备案&#xff0c;部署一个服务器&#xff0c;平常很少自己配置Nginx&#xff0c;今天记录下。 1、反向代理 正向代理 指 客户端通过代理访问后端服务 反向代理 指 服务器推出一个客户&#xff0…

Git 使用教程

一、Git的认识 1.1版本控制 什么是“版本控制”&#xff1f;我为什么要关心它呢&#xff1f; 版本控制是一种记录一个或若干文件内容变化&#xff0c;以便将来查阅特定版本修订情况的系统。 a) 还原&#xff1a;如果你是程序开发者&#xff0c;在新写一个促销活动的java文…

Elasticsearch:创建自定义 ES Rally tracks 的分步指南

作者&#xff1a;Alejandro Snchez 按照这个综合教程学习如何制作个性化的 Rally tracks ES Rally 是什么&#xff1f;它的用途是什么&#xff1f; ES Rally 是一个用于在 Elasticsearch 上测试性能的工具&#xff0c;允许你运行和记录比较测试。 做出决策可能很困难&#x…

电脑开机启动项在哪里设置?优化系统速度不是梦!

电脑的开机启动项设置直接影响着系统启动的速度和性能&#xff0c;合理配置启动项可以提高系统的启动速度&#xff0c;同时确保开机时运行的程序符合个人需求。那么&#xff0c;电脑开机启动项在哪里设置呢&#xff1f;本文将详细介绍电脑开机启动项设置的三种方法&#xff0c;…

helm部署gitlab-runner问题解决

关于.gitlab-ci.yml中build镜像时&#xff0c;docker守护进程未启动错误 参考&#xff1a;https://docs.gitlab.com/runner/install/kubernetes.html 问题截图 解决方法 values.yaml中关于conf.toml添加 注意&#xff1a;确保每个节点的docker正常运行 [[runners.kubernetes…

week04day01(爬虫)

一. 爬虫 只爬取公开的信息&#xff0c;不能爬取未公开的后台数据 1.爬虫的合法性 法无禁止皆可为 -- 属于法律的灰色地带https://www.tencent.com/robots.txt -- 网站/robots.txt 可以查看禁止爬取的内容 2. URL Uniform Resource Locator 统一资源定位符https://www.…