Angular 18+ 高级教程 – Component 组件 の Angular Component vs Custom Elements
前言
在上一篇 Angular Component vs Web Component 中,我们整体对比了 Angular Component 和 Web Component 的区别。
这一篇我们将针对 Custom Elements 的部分继续对比学习。
同样的,请先看我以前写的 DOM – Web Components の Custom Elements
Attribute、Property、Custom Event
对于一个封装好的 Custom Element,外部想与它交互有 2 个方法。
1. 修改 property 或 attribute
2. 监听 custom event
对于 Angular Component 也是如此,只是在 Component 内部,Angular 替我们封装了繁琐的实现代码。
Step by Step
Cookie Acknowledge Component Final Look
我们做一个 cookie acknowledge 组件,长这样:
调用 Cookie Acknowledge Component
<app-cookie-acknowledge websiteName="兴杰 Blog" (acknowledge)="alert($event)"></app-cookie-acknowledge>
websiteName 是组件的 attribute/property。它是组件中 paragraph Welcome to "兴杰 Blog" 的一部分。
下面这句是 Angular 的 binding syntax
(acknowledge)="alert($event)"
后面的章节会详细介绍,这里只要知道它相等于 addEventListener 就可以了。
document.querySelector('app-cookie-acknowledge')!.addEventListener('acknowledge', (event: string) => alert(event))
注:Angular 没有强制使用 CustomEvent,dispatch 的 event 可以是单纯一个 string value,不一定要是 event 对象。
Create Cookie Acknowledge Component
ng g c cookie-acknowledge
@Input and @Output
cookie-acknowledge-component.ts
export class CookieAcknowledgeComponent {
@Input()
websiteName!: string; @Output('acknowledge')
acknowledgeEmitter = new EventEmitter<string>();
}
@Input decorator 用于表示这个 attribute/property 是对外(HTML)开放的。使用者可以通过 websiteName="兴杰 Blog" 把 value 传进来。
Input 是输入,那 @Output 自然就是输出了。输出指的就是 dispatch event。它是一个 EventEmitter 对象,顾名思义就是用来 emit (AKA dispatch) event 的。
cookie-acknowledge-component.html
<h1>Cookie Acknowledge</h1>
<p>Welcome to {{ websiteName }}, to allow we track you, please press acknowledge button.</p>
<button (click)="acknowledgeEmitter.emit('Yes, track me!')">Acknowledge</button>
{{ websiteName }} 是 binding syntax,这里先不关注这个,后面章节会介绍,简单说就是把 websiteName 属性值 textContent 填入 paragraph 中。
(click) 我们上面提到过了,它就是 addEventListening。
acknowledgeEmitter.emit('Yes, track me!'),这句就是 dispatch event。
CustomEvent passing value 是通过 event.detail 属性,而 Angular 没有这个要求,我们可以直接 dispatch 任何类型的 value,当然若想 dispatch 一个 CustomEvent 也是可以。
小结
Angular 组件和 Custom Elements 一样,都是通过 component attribute/property 和 listen event dispatch 与组件交互。
Angular 用 decorator @Input 和 @Output 声明对外(HTML)开放的属性和可监听的事件,并通过 EventEmitter 对象 dispatch event。
Angular 的 event 没有要求必须是 Event 对象,我们可以 dispatch 任何类型的 value 作为 event。
@Input (required、transform)
首先,再重复一次,@Input 负责的是从 html 的 attribute 到组件对象 property 的关系处理。
我们知道 Attribute 和 Property 是两个不同的东西。不清楚的可以参考这篇 Attribute 和 Property 的区别。
比如一个原生 HTML Input Element
<input id="my-input" readonly>
<input id="my-input" readonly="whatever">
<input id="my-input" readonly="">
<input id="my-input" readonly="false">
虽然 4 个 input 里,readonly attribute 的值都不一样,但是最终它们的 readOnly property 都是 true。
这中间就是 Input element 搞的鬼了。对它来说,无论 readonly 这个 attribute 的值是什么,只要有这个 attribute,那么 property 的值就是 true。
另一个场景是类型转换,由于 attribute 的值一定是 string,而 property 值却可以是任何类型,所以中间经常需要一个 transform 的工作,它也是由 Custom Element / Component 负责。
Angular 替我们分担了很多这类的工作,来看例子吧。
export class TestInputOutputComponent {
@Input()
stringValue!: string;
}
组件需要一个 string property
<app-test-input-output stringValue="value"></app-test-input-output>
外部传 string 进来,这个例子是最简单的,我们啥也不用做。
transform
@Input()
boolValue!: boolean;
把 property 换成 boolean
<app-test-input-output boolValue="true"></app-test-input-output>
<app-test-input-output boolValue="1"></app-test-input-output>
这时不管我们写什么 string 都没用,它一定报错
从前最常见的解决方法是使用模板语法
<app-test-input-output [boolValue]="true"></app-test-input-output>
attribute 加上方括弧后,value 就变成了 JS。true 就不是 string 而是 boolean 类型了。
虽然这招可用,但是它有点间接,因为模板语法功能很强大,特地拿来做区区的类型转换有点大材小用了。所以 Angular 才特意设计一个新方法 transform。
@Input({ transform: booleanAttribute })
boolValue!: boolean;
在 @Input decorator 加上一个 transform 就可以把 string 转换成 boolean 类型了。
这个 booleanAttribute 是 Angular build-in 的方法。
import { Component, Input, OnInit, booleanAttribute } from '@angular/core';
源码长这样
export function booleanAttribute(value: unknown): boolean {
return typeof value === 'boolean' ? value : (value != null && value !== 'false');
}
只是一个简单的 coercion 函数。可以看到只有当 value 是 null or undefined 或者 string = 'false' 的时候才会被转成 false,其余情况都是 true。
所以,以下所有都是 true,除了最后一条。
<app-test-input-output boolValue=""></app-test-input-output>
<app-test-input-output boolValue></app-test-input-output>
<app-test-input-output boolValue="0"></app-test-input-output>
<app-test-input-output boolValue="1"></app-test-input-output>
<app-test-input-output boolValue="abc"></app-test-input-output>
<app-test-input-output boolValue="123"></app-test-input-output>
<app-test-input-output boolValue="false"></app-test-input-output>
除了 boolean,Angular 还有一个 build-in 的 transform 叫 numberAttribute。源码长这样:
export function numberAttribute(value: unknown, fallbackValue = NaN): number {
const isNumberValue = !isNaN(parseFloat(value as any)) && !isNaN(Number(value));
return isNumberValue ? Number(value) : fallbackValue;
}
关键就是 parseFloat 咯,把 string 转成 number。
required
Input 可以声明 required。
@Input({ required: true })
boolValue!: boolean;
声明 required 后,如果没有放 attribute 就会报错。
@Attribute
@Input 是用来拿 property 的,@Attribute 是用来拿 attribute 的。
看例子:
<app-item name="iPhone 14" />
有一个 Item 组件,它有一个 attrbute name,value 是 'iPhone 14'。
在 AppItem 组件 constructor 参数使用 @Attribute decorator
export class ItemComponent {
constructor(
// 1. 在 constructor 使用 @Attribute decorator 获取 name attribute
@Attribute('name') name: string,
) {
console.log(name); // 'iPhone 14'
}
}
这样就可以拿到 attribute value 了。
这里有 2 个点要注意:
@Attribute 是 apply 在 constructor 参数,而不是像 @Input 那样 appy 在 property。
@Attribute 不可以和 @Input 撞,两者只能有一个存在。
@Attribute 没有 binding 概念,它一定是 static string value。
@Attribute 相对 @Input 来说是非常冷门的,组件一般上很少会用 @Attribute,指令 + 原生 DOM 才可能会用到 @Attribute。(指令后面章节才会教)
@Input 和 @Output Decorator 正在被放弃
Decorator 目前普遍不受待见,两大原因。
1. ECMA 把 Decorator 拆成了两个版本,而且第二个还没有定稿。
2. 函数式的天下,Decorator 自然也变成小众了。
所以,Angular 从 v14 开始就有了弃暗投明的想法。一步一步靠拢 react、vue、solid、svelte 等等前端技术。
当然所谓的靠拢只是在开发体验上,写法上不同而已,概念是靠拢不了的。
metadata 写法
metadata inputs 写法
@Component({
inputs: [
{ name: 'boolValue', alias: 'value', required: true, transform: booleanAttribute }
]
})
取代了原本的 @Input,接口都一样,只是搬家而已。
我个人是觉得没有必要这么写啦,逻辑分开有时候也很乱,建议大家还是等 Signal-based Component 吧。
Signal-based 写法
export class CardComponent {
title = input<string>();
}
上面这个就是 Signal-based Component Input 的写法。title 是属性,input 是全局函数。
这个写法和 DI 的 inject 函数非常相识。
Angular v17.1.0 正式推出了 Signal-based Input,想学可以看这篇 Signals # Signal-based Input。
Angular Component Lifecycle vs Custom Elements Lifecycle
Custom Elements Lifecycle
Custom Elements 有 3 个 Lifecycle Hook.
1. connectedCallback 当被 append to document
2. disconnectedCallback 当被 remove from document
3. attributeChangedCallback 当监听的 attributes add, remove, change value 的时候触发
Angular Component Lifecycle
Angular 有好多 Lifecycle Hook...我先介绍 5 个基本的,后面的章节还会介绍其它的。
首先,我们添加一些交互
一个 change attribute 和一个 remove element
app.component.ts
export class AppComponent {
alert = alert;
websiteName = '兴杰 Blog';
showCookieAcknowledge = true;
}
app.component.html
<div class="container">
<div class="action">
<button (click)="websiteName = 'Derrick\'s Blog'">Change Website Name</button>
<button (click)="showCookieAcknowledge = false">Delete Cookie Acknowledge</button>
</div>
@if (showCookieAcknowledge) {
<app-cookie-acknowledge [websiteName]="websiteName" (acknowledge)=" alert($event)"></app-cookie-acknowledge>
}
</div>
不要在意 @if 和 [websiteName],后面章节会教,我们 focus Lifecycle Hook 就好了。
添加 5 个 Lifecycle Hook 到 Cookie Acknowledge Component
cookie-acknowledge.component.ts
export class CookieAcknowledgeComponent
implements OnInit, OnChanges, OnDestroy, AfterViewInit
{
@Input({ required: true })
websiteName!: string; @Output('acknowledge')
acknowledgeEmitter = new EventEmitter<string>(); constructor() {
console.log('constructor', '@Input value not ready yet');
console.log(
'constructor, this.websiteName === undefined',
this.websiteName === undefined
);
} ngOnInit(): void {
console.log('OnInit', '@Input value ready');
console.log(
'OnInit, this.websiteName !== undefined',
this.websiteName !== undefined
);
} ngOnChanges(changes: SimpleChanges): void {
if ('websiteName' in changes) {
const change = changes['websiteName'];
if (change.firstChange) {
console.log(
'OnChanges first change',
`prev: ${change.previousValue}, curr: ${change.currentValue}`
);
console.log(
'OnChanges first change, paragraph appended',
document.querySelector('.paragraph') !== null
);
console.log(
'OnChanges first change, paragraph data binding no complete yet',
document.querySelector('.paragraph')!.textContent === ''
);
} else {
console.log(
'OnChanges second change',
`previous value : ${change.previousValue}`
);
console.log(
'OnChanges second change',
`current value : ${change.currentValue}`
);
}
}
} ngAfterViewInit(): void {
console.log(
'AfterViewInit, paragraph data binding completed',
document.querySelector('.paragraph')!.textContent !== ''
);
} ngOnDestroy(): void {
console.log('OnDestroy', `element has been removed`);
console.log(
'OnDestroy, query app-cookie-acknowledge === null',
document.querySelector('app-cookie-acknowledge') === null
);
}
}
不需要看 code, 下面我们看 runtime console 就可以了。
1. constructor
组件是 class,第一个被 call 的自然是 constructor。在这个阶段 @Input 的是还没有输入值的,它是 undefined。
template 也还没有 append 到 document 里。
2. ngOnChanges (first time)
ngOnChanges 对应 Custom Elements 的 attributeChangedCallback。每当 attribute 变化的时候就会 call。
websiteName 从开始的 undefined 变成 '兴杰 blog' 后就会触发 first time onchanges。
注:Custom Elements 的 attributeChangedCallback 是没有 first time call 的,它只有后续改变才会 call。
另外,template 在这个阶段已经 append to document 了,但如果有 binding data 的部分则还没有完成。
比如这句
<p class="paragraph">Welcome to {{ websiteName }}, to allow we track you, please press acknowledge button.</p>
假如这个阶段我们 document.query .paragraph 会获得 element,但是 element.textContent 将会是 empty string。
3. ngOnInit
ngOnInit 对应 Custom Elements connectedCallback。在这个阶段 @Input 的 value 已经有值了。
通常我们会在这个阶段发 ajax 取 data 什么的。
这个阶段 binding data 依旧还没开始,paragraph.textContent 依然是 emtpty string。
4. ngAfterViewInit
这个阶段 binding data 就完成了。paragraph.textContent 已经有包括 websiteName '兴杰 blog' 在内的 text 了。
在这个阶段我们不应该再去修改 view model 了,如果修改它会报错的。
如果真的有需要修改的话,那么就用 setTimeout 让它开启下一个循环。
5. ngOnChanges(second time)
当 @Input 值被修改后,又会触发 ngOnChanges。记得,这个阶段 data binding 是还没有完成的哦。
如果想监听到 data binding 完成,可以使用 ngAfterViewChecked,但这个比较冷门,我不想在这里展开,以后的章节会详解介绍。
6. ngOnDestroy
ngOnDestroy 对应 Custom Elements 的 disconnectedCallback,这个阶段 element 已经从 document 移除了。
我们通常会在这里做一些释放资源的动作。
console 结果
Future (Signal-based Components)
参考: Github – Sub-RFC 3: Signal-based Components
这篇提到的 @Input @Output 还有 Lifecycle Hook 写法,在未来(一年后)会有很大的变化。
因为 Angular 正在向 React 学习,希望透过改变开放体验来吸引一些新用户。
感受一下:
@Input
Angular v17.1.0 正式推出了 Input Signal,想学可以看这篇 Signals。
@Output
Lifecycle Hook
改变的方向是尽可能移除 Decorator 和增加函数式特性,同时减少面向对象特性。
虽然写法上区别很大,但是底层思路改变的不多,而且 Angular 依然会保留目前的写法很长一度时间(maybe 2 more years)。
所以短期内大家还是可以学习和安心使用的。
目录
上一篇 Angular 18+ 高级教程 – Component 组件 の Angular Component vs Web Component
下一篇 Angular 18+ 高级教程 – Component 组件 の Angular Component vs Shadow DOM (CSS Isolation & slot)
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding
Angular 18+ 高级教程 – Component 组件 の Angular Component vs Custom Elements的更多相关文章
- Angular CLI 使用教程指南参考
Angular CLI 使用教程指南参考 Angular CLI 现在虽然可以正常使用但仍然处于测试阶段. Angular CLI 依赖 Node 4 和 NPM 3 或更高版本. 安装 要安装Ang ...
- Angular入门到精通系列教程(7)- 组件(@Component)基本知识
1. 概述 2. 创建Component 组件模板 视图封装模式 特殊的选择器 :host inline-styles 3. 总结 环境: Angular CLI: 11.0.6 Angular: 1 ...
- Vue教程:组件Component详解(六)
一.什么是组件? 组件 (Component) 是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功 ...
- MVC+EFCore 完整教程18 -- 升级分布视图至 View Component
之前我们详细介绍过分布视图(partial view),在有一些更加复杂的场景下,.net core为我们提供了更加强大的组件 view component, 可以认为view component是 ...
- angular2 学习笔记 ( Component 组件)
refer : https://angular.cn/docs/ts/latest/guide/template-syntax.html https://angular.cn/docs/ts/late ...
- 一篇文章看懂angularjs component组件
壹 ❀ 引 我在 angularjs 一篇文章看懂自定义指令directive 一文中详细介绍了directive基本用法与完整属性介绍.directive是个很神奇的存在,你可以不设置templa ...
- Angular 英雄示例教程
英雄指南教程(Tour of Heroes)涵盖了 Angular 的基本知识. 在本教程中,你将构建一个应用,来帮助人事代理机构来管理一群英雄. 这个入门级 app 包含很多数据驱动的应用所需的特性 ...
- [从 0 开始的 Angular 生活]No.38 实现一个 Angular Router 切换组件页面(一)
前言 今天是进入公司的第三天,为了能尽快投入项目与成为团队可用的战力,我正在努力啃官方文档学习 Angular 的知识,所以这一篇文章主要是记录我如何阅读官方文档后,实现这个非常基本的.带导航的网页应 ...
- NgRx/Store 4 + Angular 5使用教程
这篇文章将会示范如何使用NgRx/Store 4和Angular5.@ngrx/store是基于RxJS的状态管理库,其灵感来源于Redux.在NgRx中,状态是由一个包含action和reducer ...
- Component(组件)
1.Component是一个模板的控制类用于处理应用和逻辑页面的视图部分. 2.Component时Angular2应用最基础的建筑砖块. 3.任何一个Component都是NgModule的一部分, ...
随机推荐
- 2024-07-17:用go语言,给定一个整数数组nums, 我们可以重复执行以下操作: 选择数组中的前两个元素并删除它们, 每次操作得到的分数是被删除元素的和。 在保持所有操作的分数相同的前提下,
2024-07-17:用go语言,给定一个整数数组nums, 我们可以重复执行以下操作: 选择数组中的前两个元素并删除它们, 每次操作得到的分数是被删除元素的和. 在保持所有操作的分数相同的前提下, ...
- [oeasy]python0140_导入_import_from_as_namespace_
导入import 回忆上次内容 上次学习了 try except 注意要点 半角冒号 缩进 输出错误信息 有错就报告 不要隐瞒 否则找不到出错位置 还可以用traceback把 系统报错信息原 ...
- [oeasy]python0037_电传打字机_打印头_print_head_carriage_词源
换行回车 回忆上次内容 上次我们 diy了 自己的小动物 还可以 让小动物 变色.报时 还可以 说些话 这很亚文化 很酷炫的亚文化 不是吗? 回忆一下 最开始 研究报时 的 时候 回到 本行行头 的 ...
- 题解:AT_abc359_e [ABC359E] Water Tank
背景 中考结束了,但是暑假只有一天,这就是我现在能在机房里面写题解的原因-- 分析 这道题就是个单调栈. 题目上问你第一滴水流到每个位置的时间.我们考虑,答案其实就是比当前木板高且距离当前木板最近的那 ...
- C语言数据类型转换(自动类型转换+强制类型转换)
自动类型转换 1) 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换,例如: float f = 100; int n = f; f 是 float 类型的数据,需要先转换为 int 类 ...
- 关于我自己的Gui界面库完善
仓库地址:https://gitee.com/GPRO/Gui 功能说明: 解析XML, 接入AngleScript. 接下来需要做的: 因为有了WPF,MFC,HTML甚至UE5的使用经验,这里我 ...
- 七天.NET 8操作SQLite入门到实战 - 第七天Blazor学生管理页面编写和接口对接(3)
前言 本章节我们的主要内容是完善Blazor学生管理页面的编写和接口对接. 七天.NET 8 操作 SQLite 入门到实战详细教程 第一天 SQLite 简介 第二天 在 Windows 上配置 S ...
- 使用python对Excel表格某个区域保存为图片
实际工作中,我们经常会把表格某个区域(如:A1:F5)或某个图形保存为图片,如何用python自动做到这一点?不知屏幕前的小伙伴有没有遇到过类似的需求,此刻脑海里有木有一丢丢思路. python操作e ...
- 论文:使用分层强化学习进行空对空格斗(战斗机空对空搏斗)《Hierarchical Reinforcement Learning for Air-to-Air Combat》
- 为什么美国人聊天,结尾的时候他们会说“peace”
相关: https://www.youtube.com/watch?v=w2O--Ly0aQg 作为世界上当前唯一的军事霸权国家,美国居然通过立法的形式来支持种族清洗,这样的国家用虚假的"自 ...