Angular 元素是打包为自定义元素(也称为 Web Components)的 Angular 组件,这是一种与框架无关的、用于定义新 HTML 元素的 Web 标准。
自定义元素是一项 Web 平台功能,适用于 Angular 支持的所有浏览器。自定义元素通过允许你定义一个由 JavaScript 代码创建并控制其内容的标签来扩展 HTML。浏览器维护着一个已定义自定义元素的 CustomElementRegistry,它将可实例化的 JavaScript 类映射到 HTML 标签。
@angular/elements 包导出了一个 createCustomElement() API,它提供了从 Angular 组件接口和变更检测功能到内置 DOM API 的桥梁。
将组件转换为自定义元素使所有所需的 Angular 基础设施对浏览器可用。创建自定义元素非常简单直接,并能自动将组件定义的视图与变更检测和数据绑定连接起来,从而将 Angular 功能映射到对应的内置 HTML 等效物。
使用自定义元素
自定义元素是自启动的——它们在被添加到 DOM 时启动,在从 DOM 中移除时销毁。一旦自定义元素被添加到任何页面的 DOM 中,它的外观和行为就会像任何其他 HTML 元素一样,并且不需要了解任何关于 Angular 术语或使用约定的特殊知识。
要将 @angular/elements 包添加到你的工作区,请运行以下命令
工作原理
createCustomElement() 函数将组件转换为可以作为自定义元素注册到浏览器的类。在将配置好的类注册到浏览器的自定义元素注册表后,就可以在直接添加到 DOM 的内容中,像使用内置 HTML 元素一样使用这个新元素。
<my-popup message="Use Angular!"></my-popup>
当你的自定义元素被放置在页面上时,浏览器会创建注册类的实例并将其添加到 DOM 中。内容由组件的模板提供,该模板使用 Angular 模板语法,并使用组件和 DOM 数据进行渲染。组件中的输入属性(Input properties)对应于该元素的输入特性(input attributes)。
将组件转换为自定义元素
Angular 提供了 createCustomElement() 函数,用于将 Angular 组件连同其依赖项一起转换为自定义元素。
转换过程实现了 NgElementConstructor 接口,并创建了一个构造函数类,该类经配置可生成组件的自启动实例。
使用浏览器原生的 customElements.define() 函数,将配置好的构造函数及其关联的自定义元素标签注册到浏览器的 CustomElementRegistry。当浏览器遇到已注册元素的标签时,它会使用该构造函数来创建自定义元素实例。
重要提示: 避免使用组件的选择器(selector)作为自定义元素的标签名称。这可能会导致意外行为,因为 Angular 会为一个 DOM 元素创建两个组件实例:一个是常规 Angular 组件,另一个是使用自定义元素的组件。
映射
一个自定义元素宿主着一个 Angular 组件,在组件中定义的数据和逻辑与标准 DOM API 之间提供桥梁。组件的属性(properties)和逻辑会直接映射到 HTML 特性(attributes)和浏览器的事件系统中。
创建 API 会解析组件以寻找输入属性(input properties),并为自定义元素定义相应的特性(attributes)。它会转换属性名称,使其与不区分大小写的自定义元素兼容。生成的特性名称将使用连字符分隔的小写形式。例如,对于一个具有
inputProp = input({alias: 'myInputProp'})的组件,对应的自定义元素会定义一个my-input-prop特性。组件的输出(Output)会作为 HTML 自定义事件 派发,自定义事件的名称与输出名称相匹配。例如,对于一个具有
valueChanged = output()的组件,对应的自定义元素会派发名为 "valueChanged" 的事件,并且发出的数据会存储在事件的detail属性中。如果你提供了别名,则会使用该别名值;例如,clicks = output<string>({alias: 'myClick'});将导致派发名为 "myClick" 的事件。
欲了解更多信息,请参阅 Web Component 关于 创建自定义事件 的文档。
示例:弹窗服务 (Popup Service)
以前,当你想在运行时向应用程序添加组件时,必须定义一个动态组件,然后必须加载它、将其附加到 DOM 中的元素上,并连接所有的依赖项、变更检测和事件处理。
使用 Angular 自定义元素可以让这个过程更简单、更透明,因为它自动提供了所有的基础设施和框架——你所要做的就是定义你想要的事件处理方式。(如果你不打算在应用程序中直接使用该组件,仍需要将其排除在编译之外。)
以下弹窗服务(Popup Service)示例程序定义了一个组件,你既可以动态加载它,也可以将其转换为自定义元素。
| 文件 | 详情 |
|---|---|
popup.ts |
定义了一个简单的弹窗元素,它显示一条输入消息,并带有某些动画和样式。 |
popup.service.ts |
创建一个可注入的服务,该服务提供两种不同的方式来调用 Popup:作为动态组件,或者作为自定义元素。注意动态加载方法需要多少额外的设置。 |
app.ts |
定义了应用程序的根组件,该组件使用 PopupService 在运行时将弹窗添加到 DOM 中。当应用程序运行时,根组件的构造函数会将 Popup 转换为自定义元素。 |
为了进行对比,此演示展示了这两种方法。一个按钮使用动态加载方法添加弹窗,另一个使用自定义元素。结果是相同的,但准备工作有所不同。
自定义元素的类型定义
通用的 DOM API,例如 document.createElement() 或 document.querySelector(),会返回适合指定参数的元素类型。例如,调用 document.createElement('a') 会返回一个 HTMLAnchorElement,TypeScript 知道它有一个 href 属性。类似地, document.createElement('div') 会返回一个 HTMLDivElement,TypeScript 知道它没有 href 属性。
当使用未知元素(例如我们示例中的自定义元素名称 popup-element)调用这些方法时,它们会返回一个通用类型,例如 HTMLElement,因为 TypeScript 无法推断出返回元素的正确类型。
使用 Angular 创建的自定义元素继承自 NgElement(而后者又继承自 HTMLElement)。此外,这些自定义元素对于对应组件的每个输入,都将拥有一个属性。例如,我们的 popup-element 具有一个类型为 string 的 message 属性。
如果你想为自定义元素获取正确的类型,有几种选择。假设你基于以下组件创建了一个 my-dialog 自定义元素
@Component(/* ... */)
class MyDialog {
content = input('');
}
获取精确类型定义最直接的方法是将相关 DOM 方法的返回值断言(cast)为正确的类型。为此,可以使用 NgElement 和 WithProperties 类型(均由 @angular/elements 导出)
const aDialog = document.createElement('my-dialog') as NgElement &
WithProperties<{content: string}>;
aDialog.content = 'Hello, world!';
aDialog.content = 123; // <-- ERROR: TypeScript knows this should be a string.
aDialog.body = 'News'; // <-- ERROR: TypeScript knows there is no `body` property on `aDialog`.
这是为你的自定义元素快速获取 TypeScript 特性(如类型检查和自动完成支持)的好方法。但如果你在多个地方需要它,这会变得很繁琐,因为你必须在每次出现时都对返回类型进行断言。
另一种方法是扩展 HTMLElementTagNameMap,它只需要定义一次每个自定义元素的类型。TypeScript 使用它根据标签名称推断返回元素的类型(用于诸如 document.createElement()、document.querySelector() 等 DOM 方法)
declare global {
interface HTMLElementTagNameMap {
'my-dialog': NgElement & WithProperties<{content: string}>;
'my-other-element': NgElement & WithProperties<{foo: 'bar'}>;
…
}
}
现在,TypeScript 可以像对内置元素一样推断出正确的类型
document.createElement('div'); //--> HTMLDivElement (built-in element)
document.querySelector('foo'); //--> Element (unknown element)
document.createElement('my-dialog'); //--> NgElement & WithProperties<{content: string}> (custom element)
document.querySelector('my-other-element'); //--> NgElement & WithProperties<{foo: 'bar'}> (custom element)
局限性
由于与 disconnect() 回调相关的问题,在销毁然后重新附加使用 @angular/elements 创建的自定义元素时应格外小心。你可能会遇到此问题的情况包括:
- 在
AngularJS中使用ng-if或ng-repeat渲染组件 - 手动将元素从 DOM 中拆卸并重新附加