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

这篇具有很好参考价值的文章主要介绍了[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:

[Angular 基础] - 自定义指令,深入学习 directive,# Angular,angular.js,学习,javascript

目前项目结构如下,这里 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';
  }
}

效果如下:

[Angular 基础] - 自定义指令,深入学习 directive,# Angular,angular.js,学习,javascript

这里简述一下修改:

  • 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'
    );
  }
}

显示效果如下:

[Angular 基础] - 自定义指令,深入学习 directive,# Angular,angular.js,学习,javascript

@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'
    );
  }
}

效果如下:

[Angular 基础] - 自定义指令,深入学习 directive,# Angular,angular.js,学习,javascript

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

  • 如果不传递 ['$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>

效果如下:

[Angular 基础] - 自定义指令,深入学习 directive,# Angular,angular.js,学习,javascript

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

  • 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 我就不贴代码了

效果如下:

[Angular 基础] - 自定义指令,深入学习 directive,# Angular,angular.js,学习,javascript

下面进入实现的分析部分

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;
  }
}

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

[Angular 基础] - 自定义指令,深入学习 directive,# Angular,angular.js,学习,javascript


补充一下点击 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) {}
}

[Angular 基础] - 自定义指令,深入学习 directive,# Angular,angular.js,学习,javascript

reference

没有特殊标注的都是来自官方文档的内容文章来源地址https://www.toymoban.com/news/detail-832217.html

  • Structural directives
  • TemplateRef
  • ViewContainerRef

到了这里,关于[Angular 基础] - 自定义指令,深入学习 directive的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Angular 指令介绍及使用(三)

    在 Angular 中,指令是一种机制,用于扩展和修改组件的行为和外观。指令可以由开发者自定义,也可以是 Angular 框架自带的一些内置指令。通过使用指令,我们可以在 HTML 模板中通过属性或元素名来操作组件。 Angular 中的指令主要分为三种类型: 组件指令(Component Directives)

    2024年02月05日
    浏览(37)
  • angular中如何定义一个全局组件?

    需求,我们需要新建一个navBreadcrumb的全局组件。这是一个面包屑导航,在不同的页面引入时传入一个路由数组即可。 第一步:我们新建这个组件: 这里有几个要注意的点: 1 ---nav-breadcrumb.module----------这个文件时一定要的。angular和vue/react不同,modulecomponent,对于任何页面而

    2024年02月12日
    浏览(40)
  • 深入探索Angular:构建现代化的Web应用

    Angular是一种流行的前端开发框架,被广泛用于构建现代化的Web应用。作为一名技术专家,我们需要深入了解Angular的核心原则和设计思想,并学会如何应用它来构建出色的Web应用。本篇博客将详细介绍Angular,包括其基本概念、关键组件以及实际案例。 Angular是一个基于TypeScr

    2024年02月12日
    浏览(73)
  • angular框架简介基础与使用(全文2w8字)前端框架angular

    本文的所有内容,可以在我的博客上看到,下面是地址。建议去博客看,因为csdn的这篇图片我没上传。 可以转载,但请注明出处 我的博客—点击跳转 https://numb.run Angular是谷歌开发的一款开源的web前端框架,诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀

    2024年02月02日
    浏览(45)
  • 深入Angular:(转&翻)Do you still think that NgZone (zone.js) is required for change detection in Angular?

    原文链接:IndepthApp 前言概览:注意区分NgZone和zone.js, 更多细节在Angular跟新策略篇,尚未翻译完成。 本文主要解释了Angular是如何基于zone.js实现NgZone。 同时阐述如何在不使用zone.js的情况下,实现手动更新。文章最后部分将描述自动跟新策略何时会失效。 我看过的大多数文章

    2024年01月16日
    浏览(54)
  • angular实现自定义模块路由懒加载;配置自定义模块路由及子路由

    图片中绿色表示新建的文件;黄色表示被更改的文件; 1、创建一个新的项目 2、创建一个用户模块,并配置路由 如图: 3 、在module/模块下创建user组件 如图: 4、实现路由懒加载 依次修改下列几个文件: #1 user-routing.module.ts

    2024年02月09日
    浏览(36)
  • Angular 怎么封装基础组件?

    在Angular中,封装基础组件通常是通过创建自定义Angular组件来实现的。这些自定义组件可以包含通用的功能和样式,然后可以在应用中多次重复使用。以下是一个简单的示例,说明如何封装一个基础组件。 假设我们要创建一个通用的警告框组件,它可以在应用中的不同部分显

    2024年02月09日
    浏览(37)
  • 如何在 Angular 中为响应式表单创建自定义验证器

    简介 Angular 的 @angular/forms 包提供了一个 Validators 类,支持诸如 required 、 minLength 、 maxLength 和 pattern 等有用的内置验证器。然而,可能存在需要更复杂或自定义规则进行验证的表单字段。在这种情况下,您可以使用自定义验证器。 在 Angular 中使用响应式表单时,您可以使用函

    2024年02月22日
    浏览(50)
  • [Angular 基础] - 生命周期函数

    之前的笔记: [Angular 基础] - Angular 渲染过程 组件的创建 [Angular 基础] - 数据绑定(databinding) [Angular 基础] - 指令(directives) 以上为静态页面,即不涉及到跨组件交流的内容 以下涉及到组件内的沟通,从这开始数据就“活”了 [Angular 基础] - 自定义事件 自定义属性 [Angular 基础]

    2024年02月20日
    浏览(41)
  • Angular16的路由守卫基础使用

    使用 ng generate guard /guard/login 命令生成guard文件 因新版Angular取消了CanActivate的使用,改用CanActivateFn,因此使用router跳转需要通过inject的方式导入。 在路由文件中,对需要守卫的路由地址配置guard

    2024年02月12日
    浏览(33)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包