Angular 元素就是打包成自定義元素的 Angular 組件。所謂自定義元素就是一套與具體框架無關(guān)的用于定義新 HTML 元素的 Web 標(biāo)準(zhǔn)。
這節(jié)的范例應(yīng)用,請參閱現(xiàn)場演練 / 下載范例。
自定義元素這項(xiàng)特性目前受到了 Chrome、Edge(基于 Chromium 的版本)、Opera 和 Safari 的支持,在其它瀏覽器中也能通過膩?zhàn)幽_本加以支持。 自定義元素?cái)U(kuò)展了 HTML,它允許你定義一個(gè)由 JavaScript 代碼創(chuàng)建和控制的標(biāo)簽。 瀏覽器會維護(hù)一個(gè)自定義元素的注冊表 ?CustomElementRegistry
?,它把一個(gè)可實(shí)例化的 JavaScript 類映射到 HTML 標(biāo)簽上。
?@angular/elements
? 包導(dǎo)出了一個(gè) ?createCustomElement()
? API,它在 Angular 組件接口與變更檢測功能和內(nèi)置 DOM API 之間建立了一個(gè)橋梁。
把組件轉(zhuǎn)換成自定義元素可以讓所有所需的 Angular 基礎(chǔ)設(shè)施都在瀏覽器中可用。 創(chuàng)建自定義元素的方式簡單直觀,它會自動把你組件定義的視圖連同變更檢測與數(shù)據(jù)綁定等 Angular 的功能映射為相應(yīng)的內(nèi)置 HTML 等價(jià)物。
我們正在持續(xù)開發(fā)自定義元素功能,讓它們可以用在由其它框架所構(gòu)建的 Web 應(yīng)用中。 Angular 框架的一個(gè)小型的、自包含的版本將會作為服務(wù)注入進(jìn)去,以提供組件的變更檢測和數(shù)據(jù)綁定功能。 要了解這個(gè)開發(fā)方向的更多內(nèi)容,參閱這個(gè)視頻演講。
自定義元素會自舉 —— 它們在添加到 DOM 中時(shí)就會自行啟動自己,并在從 DOM 中移除時(shí)自行銷毀自己。一旦自定義元素添加到了任何頁面的 DOM 中,它的外觀和行為就和其它的 HTML 元素一樣了,不需要對 Angular 的術(shù)語或使用約定有任何特殊的了解。
把組件轉(zhuǎn)換成自定義元素為你在 Angular 應(yīng)用中創(chuàng)建動態(tài) HTML 內(nèi)容提供了一種簡單的方式。 在 Angular 應(yīng)用中,你直接添加到 DOM 中的 HTML 內(nèi)容是不會經(jīng)過 Angular 處理的,除非你使用動態(tài)組件來借助自己的代碼把 HTML 標(biāo)簽與你的應(yīng)用數(shù)據(jù)關(guān)聯(lián)起來并參與變更檢測。而使用自定義組件,所有這些裝配工作都是自動的。
如果你有一個(gè)富內(nèi)容應(yīng)用(比如正在展示本文檔的這個(gè)),自定義元素能讓你的內(nèi)容提供者使用復(fù)雜的 Angular 功能,而不要求他了解 Angular 的知識。比如,像本文檔這樣的 Angular 指南是使用 Angular 導(dǎo)航工具直接添加到 DOM 中的,但是其中可以包含特殊的元素,比如 ?<code-snippet>
?,它可以執(zhí)行復(fù)雜的操作。 你所要告訴你的內(nèi)容提供者的一切,就是這個(gè)自定義元素的語法。他們不需要了解關(guān)于 Angular 的任何知識,也不需要了解你的組件的數(shù)據(jù)結(jié)構(gòu)或?qū)崿F(xiàn)。
使用 ?createCustomElement()
? 函數(shù)來把組件轉(zhuǎn)換成一個(gè)可注冊成瀏覽器中自定義元素的類。 注冊完這個(gè)配置好的類之后,就可以在內(nèi)容中像內(nèi)置 HTML 元素一樣使用這個(gè)新元素了,比如直接把它加到 DOM 中:
<my-popup message="Use Angular!"></my-popup>
當(dāng)你的自定義元素放進(jìn)頁面中時(shí),瀏覽器會創(chuàng)建一個(gè)已注冊類的實(shí)例。其內(nèi)容是由組件模板提供的,它使用 Angular 模板語法,并且使用組件和 DOM 數(shù)據(jù)進(jìn)行渲染。組件的輸入屬性(Property)對應(yīng)于該元素的輸入屬性(Attribute)。
Angular 提供了 ?createCustomElement()
? 函數(shù),以支持把 Angular 組件及其依賴轉(zhuǎn)換成自定義元素。該函數(shù)會收集該組件的 ?Observable
?型屬性,提供瀏覽器創(chuàng)建和銷毀實(shí)例時(shí)所需的 Angular 功能,還會對變更進(jìn)行檢測并做出響應(yīng)。
這個(gè)轉(zhuǎn)換過程實(shí)現(xiàn)了 ?NgElementConstructor
? 接口,并創(chuàng)建了一個(gè)構(gòu)造器類,用于生成該組件的一個(gè)自舉型實(shí)例。
使用內(nèi)置的 ?customElements.define()
? 函數(shù)把這個(gè)配置好的構(gòu)造器和相關(guān)的自定義元素標(biāo)簽注冊到瀏覽器的 ?CustomElementRegistry
?中。 當(dāng)瀏覽器遇到這個(gè)已注冊元素的標(biāo)簽時(shí),就會使用該構(gòu)造器來創(chuàng)建一個(gè)自定義元素的實(shí)例。
不要將 ?
@Component
? 的選擇器用作自定義元素的標(biāo)記名稱。由于 Angular 會為單個(gè) DOM 元素創(chuàng)建兩個(gè)組件實(shí)例,所以這可能導(dǎo)致意外行為:一個(gè)是常規(guī)的 Angular 組件,而另一個(gè)是自定義元素。
寄宿著 Angular 組件的自定義元素在組件中定義的"數(shù)據(jù)及邏輯"和標(biāo)準(zhǔn)的 DOM API 之間建立了一座橋梁。組件的屬性和邏輯會直接映射到 HTML 屬性和瀏覽器的事件系統(tǒng)中。
@Input('myInputProp') inputProp
? 的組件,其對應(yīng)的自定義元素會帶有一個(gè) ?my-input-prop
? 屬性。@Output() valueChanged = new EventEmitter()
? 屬性的組件,其相應(yīng)的自定義元素將會分發(fā)名叫 "valueChanged" 的事件,事件中所攜帶的數(shù)據(jù)存儲在該事件對象的 ?detail
?屬性中。 如果你提供了別名,就改用這個(gè)別名。比如,?@Output('myClick') clicks = new EventEmitter<string>();
? 會導(dǎo)致分發(fā)名為 "myClick" 事件。要了解更多,請參閱 Web Components 的文檔:Creating custom events。
最近開發(fā)的 Web 平臺特性:自定義元素目前在一些瀏覽器中實(shí)現(xiàn)了原生支持,而其它瀏覽器或者尚未決定,或者已經(jīng)制訂了計(jì)劃。
瀏覽器 |
自定義元素支持 |
---|---|
Chrome |
原生支持。 |
Edge (基于 Chromium 的版本) |
原生支持。 |
Firefox |
原生支持。 |
Opera |
原生支持。 |
Safari |
原生支持。 |
要往工作空間中添加 ?@angular/elements
? 包,請運(yùn)行如下命令:
npm install @angular/elements --save
以前,如果你要在運(yùn)行期間把一個(gè)組件添加到應(yīng)用中,就得定義成動態(tài)組件,然后還要加載它、把它附加到 DOM 中的元素上,并且裝配所有的依賴、變更檢測和事件處理,詳見動態(tài)組件加載器。
用 Angular 自定義組件會讓這個(gè)過程更簡單、更透明。它會自動提供所有基礎(chǔ)設(shè)施和框架,而你要做的就是定義所需的各種事件處理邏輯。(如果你不準(zhǔn)備在應(yīng)用中直接用它,還要把該組件在編譯時(shí)排除出去。)
這個(gè)彈窗服務(wù)的范例應(yīng)用(見后面)定義了一個(gè)組件,你可以動態(tài)加載它也可以把它轉(zhuǎn)換成自定義組件。
popup.component.ts
? 定義了一個(gè)簡單的彈窗元素,用于顯示一條輸入消息,附帶一些動畫和樣式。popup.service.ts
? 創(chuàng)建了一個(gè)可注入的服務(wù),它提供了兩種方式來執(zhí)行 ?PopupComponent
?:作為動態(tài)組件或作為自定義元素。注意動態(tài)組件的方式需要更多的代碼來做搭建工作。app.module.ts
? 把 ?PopupComponent
?添加到模塊的 ?entryComponents
?列表中,而從編譯過程中排除它,以消除啟動時(shí)的警告和錯(cuò)誤。app.component.ts
? 定義了該應(yīng)用的根組件,它借助 ?PopupService
?在運(yùn)行時(shí)把這個(gè)彈窗添加到 DOM 中。在應(yīng)用運(yùn)行期間,根組件的構(gòu)造函數(shù)會把 ?PopupComponent
?轉(zhuǎn)換成自定義元素。為了對比,這個(gè)范例中同時(shí)演示了這兩種方式。一個(gè)按鈕使用動態(tài)加載的方式添加彈窗,另一個(gè)按鈕使用自定義元素的方式??梢钥吹剑瑑烧叩慕Y(jié)果是一樣的,其差別只是準(zhǔn)備過程不同。
import { Component, EventEmitter, HostBinding, Input, Output } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';
@Component({
selector: 'my-popup',
template: `
<span>Popup: {{message}}</span>
<button (click)="closed.next()">✖</button>
`,
animations: [
trigger('state', [
state('opened', style({transform: 'translateY(0%)'})),
state('void, closed', style({transform: 'translateY(100%)', opacity: 0})),
transition('* => *', animate('100ms ease-in')),
])
],
styles: [`
:host {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #009cff;
height: 48px;
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid black;
font-size: 24px;
}
button {
border-radius: 50%;
}
`]
})
export class PopupComponent {
@HostBinding('@state')
state: 'opened' | 'closed' = 'closed';
@Input()
get message(): string { return this._message; }
set message(message: string) {
this._message = message;
this.state = 'opened';
}
private _message = '';
@Output()
closed = new EventEmitter<void>();
}
import { ApplicationRef, ComponentFactoryResolver, Injectable, Injector } from '@angular/core';
import { NgElement, WithProperties } from '@angular/elements';
import { PopupComponent } from './popup.component';
@Injectable()
export class PopupService {
constructor(private injector: Injector,
private applicationRef: ApplicationRef,
private componentFactoryResolver: ComponentFactoryResolver) {}
// Previous dynamic-loading method required you to set up infrastructure
// before adding the popup to the DOM.
showAsComponent(message: string) {
// Create element
const popup = document.createElement('popup-component');
// Create the component and wire it up with the element
const factory = this.componentFactoryResolver.resolveComponentFactory(PopupComponent);
const popupComponentRef = factory.create(this.injector, [], popup);
// Attach to the view so that the change detector knows to run
this.applicationRef.attachView(popupComponentRef.hostView);
// Listen to the close event
popupComponentRef.instance.closed.subscribe(() => {
document.body.removeChild(popup);
this.applicationRef.detachView(popupComponentRef.hostView);
});
// Set the message
popupComponentRef.instance.message = message;
// Add to the DOM
document.body.appendChild(popup);
}
// This uses the new custom-element method to add the popup to the DOM.
showAsElement(message: string) {
// Create element
const popupEl: NgElement & WithProperties<PopupComponent> = document.createElement('popup-element') as any;
// Listen to the close event
popupEl.addEventListener('closed', () => document.body.removeChild(popupEl));
// Set the message
popupEl.message = message;
// Add to the DOM
document.body.appendChild(popupEl);
}
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { PopupComponent } from './popup.component';
import { PopupService } from './popup.service';
@NgModule({
imports: [BrowserModule, BrowserAnimationsModule],
providers: [PopupService],
declarations: [AppComponent, PopupComponent],
bootstrap: [AppComponent],
})
export class AppModule {
}
import { Component, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { PopupService } from './popup.service';
import { PopupComponent } from './popup.component';
@Component({
selector: 'app-root',
template: `
<input #input value="Message">
<button (click)="popup.showAsComponent(input.value)">Show as component</button>
<button (click)="popup.showAsElement(input.value)">Show as element</button>
`,
})
export class AppComponent {
constructor(injector: Injector, public popup: PopupService) {
// Convert `PopupComponent` to a custom element.
const PopupElement = createCustomElement(PopupComponent, {injector});
// Register the custom element with the browser.
customElements.define('popup-element', PopupElement);
}
}
一般的 DOM API,比如 ?document.createElement()
? 或 ?document.querySelector()
?,會返回一個(gè)與指定的參數(shù)相匹配的元素類型。比如,調(diào)用 ?document.createElement('a')
? 會返回 ?HTMLAnchorElement
?,這樣 TypeScript 就會知道它有一個(gè) ?href
?屬性,而 ?document.createElement('div')
? 會返回 ?HTMLDivElement
?,這樣 TypeScript 就會知道它沒有 ?href
?屬性。
當(dāng)調(diào)用未知元素(比如自定義的元素名 ?popup-element
?)時(shí),該方法會返回泛化類型,比如 ?HTMLELement
?,這時(shí)候 TypeScript 就無法推斷出所返回元素的正確類型。
用 Angular 創(chuàng)建的自定義元素會擴(kuò)展 ?NgElement
?類型(而它擴(kuò)展了 ?HTMLElement
?)。除此之外,這些自定義元素還擁有相應(yīng)組件的每個(gè)輸入屬性。比如,?popup-element
? 元素具有一個(gè) ?string
?型的 ?message
?屬性。
如果你要讓你的自定義元素獲得正確的類型,還可使用一些選項(xiàng)。假設(shè)你要創(chuàng)建一個(gè)基于下列組件的自定義元素 ?my-dialog
?:
@Component(...)
class MyDialog {
@Input() content: string;
}
要獲得精確類型,最直白的方式是把相關(guān) DOM 方法的返回值轉(zhuǎn)換成正確的類型。要做到這一點(diǎn),可以使用 ?NgElement
?和 ?WithProperties
?類型(都導(dǎo)出自 ?@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 特性(比如類型檢查和自動完成支持)的好辦法,不過如果你要在多個(gè)地方使用它,可能會有點(diǎn)啰嗦,因?yàn)椴坏貌辉诿總€(gè)地方對返回類型做轉(zhuǎn)換。
另一種方式可以對每個(gè)自定義元素的類型只聲明一次。你可以擴(kuò)展 ?HTMLElementTagNameMap
?,TypeScript 會在 DOM 方法(如 ?document.createElement()
?、?document.querySelector()
? 等)中用它來根據(jù)標(biāo)簽名推斷返回元素的類型。
declare global {
interface HTMLElementTagNameMap {
'my-dialog': NgElement & WithProperties<{content: string}>;
'my-other-element': NgElement & WithProperties<{foo: 'bar'}>;
...
}
}
現(xiàn)在,TypeScript 就可以像內(nèi)置元素一樣推斷出它的正確類型了:
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)
更多建議: