@ionic/angularがWebComponentsでテンプレートチェックを効かせるためにやっていることを調べる
※本記事は終了した別サービスで2019年12月23日に公開した記事のリストアです。
AngularでWebComponetsライブラリを使う時の基本
CustomElementsは基本的にはHTMLElementです(正確にはHTMLElementを継承して作ったもの)。customElements.define()
することでグローバルに存在するようになるので、なんらかの方法でライブラリをインクルードすれば、あとはtemplateに書いておけば動きます。Angularから見れば、input
タグでもcustom-input
タグでも同じ話なわけです。
<input type="text"/>
<custom-input type="text"></custom-input>
ただし、Angularのテンプレートチェックに<input>
はわかるけど<custom-input>
は知らないやつだなあと怒られるので対策が必要です。
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA], ←templateチェックを無効にする
})
export class SomeModule {}
これで自由にHTML要素を書いてもよくなりますが、テンプレートの事前チェックが無効化されて実行時の存在判定に後送りされるため、他のタイポなども道連れで検出できなくなります。
<typo-input type="text"></typo-input> ←どこにも定義されてないがビルド時にエラーにならない
このようにAngularでWebComponetsを使うには誓約と制約があります。
@ionic/angularがやっていること
さて、ionicのv4はWebComponentsベースで書き直され、Angularだけでなくreactやvueでもできるようになりましたが、Angularは引き続きテンプレートチェックがビシッと効いています。
不思議なので中身を調べてみましょう。
なんと、同じ名称のAngularComponentが定義されていました❗️
ソースを読むとプロキシという扱いのようなので、これをプロキシコンポーネントと呼びます。
どうやらこいつがビルド時にAngularに差し出されているから、テンプレートの型チェックが効いてるようです。
<ion-input>
を例に引っ張ってきました。現バージョンだと@ProxyCmp
という新デコレータが作られていて、proxyMethods
やproxyInputs
の記述は省力化されていますが、構造がわかりやすいので旧スタイルで掲載しています。
export declare interface IonInput extends StencilComponents<'IonInput'> {}
@Component({
selector: 'ion-input',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: [
'accept',
'autocapitalize',
'autocomplete',
'autocorrect',
'autofocus',
'clearInput',
'clearOnEdit',
'color',
'debounce',
'disabled',
'inputmode',
'max',
'maxlength',
'min',
'minlength',
'mode',
'multiple',
'name',
'pattern',
'placeholder',
'readonly',
'required',
'size',
'spellcheck',
'step',
'type',
'value',
],
})
export class IonInput {
ionInput!: EventEmitter<CustomEvent>;
ionChange!: EventEmitter<CustomEvent>;
ionBlur!: EventEmitter<CustomEvent>;
ionFocus!: EventEmitter<CustomEvent>;
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef) {
c.detach();
this.el = r.nativeElement;
proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionBlur', 'ionFocus']);
}
}
proxyMethods(IonInput, ['setFocus', 'getInputElement']);
proxyInputs(IonInput, [
'color',
'mode',
'accept',
'autocapitalize',
'autocomplete',
'autocorrect',
'autofocus',
'clearInput',
'clearOnEdit',
'debounce',
'disabled',
'inputmode',
'max',
'maxlength',
'min',
'minlength',
'multiple',
'name',
'pattern',
'placeholder',
'readonly',
'required',
'spellcheck',
'step',
'size',
'type',
'value',
]);
全体構造
通常のAngularComponentに則ったデコレータとclassです。proxyOutputs
、proxyMethods
、proxyInputs
はionicが用意しているユーティリティ関数です。
生のWebComponentsに対してバインドする方法
接続
コンポーネントデコレータのselectorをcustomElementと同じ名称にしています。相当ハックだと思ったのですが、Angularのコンポーネントはカスタム要素としてHTMLにそのまま出力するので、selectorをHTMLに存在している要素名にするとHTMLではHTMLに存在している要素として解釈するわけなんですよね。もちろん_nghost-wke-c2
のような属性は付きます。良い子は真似するな案件ですが、customElementに限らず標準HTML要素のinput
とかspan
とかでも同じことできます。
プロキシ
コンポーネントでは、elementRefから引っ張ったnativeElementの生DOM参照をクラスプロパティに保持しています。
proxyInputsとproxyMethodsは、そのクラスプロパティに保持したnativeElementを通して生のionic-input
に対して、HTML属性のget/setやionic内部に用意されているメソッドを生やしています。
export const proxyInputs = (Cmp: any, inputs: string[]) => {
const Prototype = Cmp.prototype;
inputs.forEach(item => {
Object.defineProperty(Prototype, item, {
get() {
return this.el[item];
},
set(val: any) {
this.z.runOutsideAngular(() => (this.el[item] = val));
}
});
});
};
export const proxyMethods = (Cmp: any, methods: string[]) => {
const Prototype = Cmp.prototype;
methods.forEach(methodName => {
Prototype[methodName] = function () {
const args = arguments;
return this.z.runOutsideAngular(() =>
this.el[methodName].apply(this.el, args)
);
};
});
};
proxyOutputsは、rxjsのfromEvent
(中でaddEventListenerやremoveEventListenerしながらObservable化するやつ)を、コンポーネントの各EventEmitter型のプロパティにセットしているものです。コンポーネントclassに同名のクラスプロパティが用意されています。
export const proxyOutputs = (instance: any, el: any, events: string[]) => {
events.forEach(eventName => instance[eventName] = fromEvent(el, eventName));
}
まとめ
プロキシするコンポーネントを用意することで、グローバルに存在しているWebComponentsライブラリでも、Angularのテンプレートの中でまるでAngularComponentかのように扱えるようになり、テンプレートの型チェックが効くようになっています。
Discussion