Angular elements はAngularコンポーネントを カスタム要素(Web Componentsとも呼ばれる)としてパッケージ化したものです。これは、新しい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
パッケージを追加するには、次のコマンドを実行します。
npm install @angular/elements --save
仕組み
createCustomElement()
関数は、コンポーネントをブラウザにカスタム要素として登録できるクラスに変換します。
設定したクラスをブラウザのカスタム要素レジストリに登録した後、新しい要素を、DOMに直接追加するコンテンツ内で組み込みのHTML要素と同じように使用します。
<my-popup message="Use Angular!"></my-popup>
カスタム要素がページに配置されると、ブラウザは登録されたクラスのインスタンスを作成し、それをDOMに追加します。 コンテンツはコンポーネントのテンプレートによって提供され、Angularテンプレート構文を使用し、コンポーネントとDOMデータを使用してレンダリングされます。 コンポーネントの入力プロパティは、要素の入力属性に対応します。
コンポーネントをカスタム要素に変換する
Angularは、Angularコンポーネントとその依存関係をカスタム要素に変換するためのcreateCustomElement()
関数を提供します。
変換プロセスはNgElementConstructor
インターフェースを実装し、
コンポーネントの自己ブートストラップインスタンスを生成するように構成されたコンストラクタークラスを作成します。
ブラウザのネイティブなcustomElements.define()
関数を使用して、設定されたコンストラクターとその関連するカスタム要素タグをブラウザのCustomElementRegistry
に登録します。
ブラウザが登録された要素のタグを検出すると、コンストラクターを使用してカスタム要素インスタンスを作成します。
IMPORTANT: コンポーネントのセレクターをカスタム要素のタグ名として使用することは避けてください。 これは、Angularが単一のDOM要素に対して2つのコンポーネントインスタンスを作成するため、予期しない動作につながる可能性があります。 1つは通常のAngularコンポーネント、もう1つはカスタム要素を使用するものです。
マッピング
カスタム要素はAngularコンポーネントを_ホスト_し、コンポーネントで定義されたデータとロジック、および標準のDOM API間のブリッジを提供します。 コンポーネントのプロパティとロジックは、HTML属性とブラウザのイベントシステムに直接マッピングされます。
作成APIは、入力プロパティを探してコンポーネントを解析し、カスタム要素に対応する属性を定義します。 プロパティ名をカスタム要素と互換性があるように変換します。カスタム要素は大文字と小文字の区別を認識しません。 結果として得られる属性名は、ダッシュで区切られた小文字を使用します。 例えば、
inputProp = input({alias: 'myInputProp'})
を持つコンポーネントは、対応するカスタム要素としてmy-input-prop
という属性を定義します。コンポーネントの出力はHTMLのカスタムイベントとしてディスパッチされ、カスタムイベントの名前は出力名と一致します。 例えば、
valueChanged = output()
を持つコンポーネントの場合、対応するカスタム要素は"valueChanged"という名前のイベントをディスパッチし、出力されたデータはイベントのdetail
プロパティに格納されます。 エイリアスを提供した場合、その値が使用されます。clicks = output<string>({alias: 'myClick'});
は"myClick"という名前のディスパッチイベントを生成します。
詳細については、Webコンポーネントのドキュメントのカスタムイベントの作成を参照してください。
例: Popupサービス
以前は、実行時にコンポーネントをアプリケーションに追加したい場合、_動的コンポーネント_を定義し、それをロードしてDOMの要素にアタッチし、すべての依存関係、変更検知、イベント処理を配線する必要がありました。
Angularカスタム要素を使用すると、必要なインフラストラクチャとフレームワークをすべて自動的に提供することで、プロセスがよりシンプルかつ透過的になります。必要なのは、希望するイベント処理の種類を定義することだけです。 (アプリケーションで使用しない場合は、コンポーネントをコンパイルから除外する必要はあります。)
以下のPopupサービス例のアプリケーションは、動的にロードするかカスタム要素に変換できるコンポーネントを定義しています。
ファイル | 詳細 |
---|---|
popup.component.ts |
入力メッセージを表示するシンプルなポップアップ要素を、アニメーションとスタイル付きで定義します。 |
popup.service.ts |
PopupComponent を呼び出す2つの異なる方法を提供する注入可能サービスを作成します。動的コンポーネントとして、またはカスタム要素として。動的読み込み方式には、より多くのセットアップが必要であることに注目してください。 |
app.component.ts |
PopupService を使用して実行時にDOMにポップアップを追加する、アプリケーションのルートコンポーネントを定義します。アプリケーションの実行時、ルートコンポーネントのコンストラクターはPopupComponent をカスタム要素に変換します。 |
比較のため、デモでは両方の方法を示しています。 一方のボタンは動的読み込み方式を使用してポップアップを追加し、もう一方はカスタム要素を使用します。 結果は同じですが、準備が異なります。
popup.component.ts
import {Component, computed, input, output} from '@angular/core';import {animate, state, style, transition, trigger} from '@angular/animations';@Component({ selector: 'my-popup', template: ` <span>Popup: {{ message }}</span> <button type="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%; } `, ], host: { '[@state]': 'state()', },})export class PopupComponent { message = input(''); closed = output<void>(); state = computed(() => (this.message() ? 'opened' : 'closed'));}
popup.service.ts
import {ApplicationRef, createComponent, EnvironmentInjector, Injectable} from '@angular/core';import {NgElement, WithProperties} from '@angular/elements';import {PopupComponent} from './popup.component';@Injectable()export class PopupService { constructor( private injector: EnvironmentInjector, private applicationRef: ApplicationRef, ) {} // 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 popupComponentRef = createComponent(PopupComponent, { environmentInjector: this.injector, hostElement: 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); }}
app.component.ts
import {Component, Injector} from '@angular/core';import {createCustomElement} from '@angular/elements';import {PopupComponent} from './popup.component';import {PopupService} from './popup.service';@Component({ selector: 'app-root', template: ` <input #input value="Message" /> <button type="button" (click)="popup.showAsComponent(input.value)">Show as component</button> <button type="button" (click)="popup.showAsElement(input.value)">Show as element</button> `, providers: [PopupService], imports: [PopupComponent],})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); }}
カスタム要素の型定義
document.createElement()
やdocument.querySelector()
のような汎用DOM APIは、指定された引数に適した要素型を返します。
例えば、document.createElement('a')
を呼び出すとHTMLAnchorElement
が返され、TypeScriptはこれにhref
プロパティがあることを認識しています。
同様に、document.createElement('div')
はHTMLDivElement
を返しますが、TypeScriptはこれにhref
プロパティがないことを認識しています。
カスタム要素名(この例ではpopup-element
)のような未知の要素で呼び出された場合、TypeScriptが返された要素の正しい型を推論できないため、これらのメソッドはHTMLElement
のような汎用型を返します。
Angularで作成されたカスタム要素はNgElement
(これはさらにHTMLElement
を拡張します)を拡張します。
さらに、これらのカスタム要素は対応するコンポーネントの各入力に対応するプロパティを持ちます。
例えば、popup-element
はstring
型のmessage
プロパティを持ちます。
カスタム要素の正しい型を取得したい場合、いくつかの選択肢があります。
次のコンポーネントに基づいてmy-dialog
カスタム要素を作成すると仮定します。
@Component(…)class MyDialog { content = input(string);}
正確な型定義を取得する最も簡単な方法は、関連するDOMメソッドの戻り値を正しい型にキャストすることです。
そのためには、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)
制限事項
@angular/elements
により作成されたカスタム要素を破棄してから再アタッチする際には、disconnect()コールバックに関する問題があるため注意が必要です。この問題に遭遇する可能性のあるケースは以下の通りです。
- AngularJSにおいて
ng-if
またはng-repeat
内でコンポーネントをレンダリングする場合 - 要素をDOMから手動でデタッチし、再アタッチする場合