既存のHTML要素と同様のインターフェースを持つコンポーネントならば、新たに学習することが少なく済み、新たに開発に参加するメンバーや未来の自分が楽できるという利点があります。これを実現するためにはTypeScriptの型定義からこれを実現します。
実装の目標において最も頼りになるのはMDNのHTML要素解説です。とりあえずこれに書いてある属性通りの振る舞いができると扱いやすいです。イベントについてもいくらか書いてあります。
input: 入力欄 (フォーム入力) 要素 – HTML: HyperText Markup Language | MDN
イベントに関してはふんわりしていて申し訳ないですが一覧としては次のリファレンスが有効です。
イベントリファレンス | MDN#標準イベント
内部実装において最も頼りになるファイルはTypeScriptインストール時に付随するファイルであるlib.dom.d.tsです。これには既存のHTML上(DOM)で起こることのほぼ全てが型定義されています(網羅しているかの確認はしていませんが、今のところ私は知っててlib.dom.d.tsが知らないDOM上の型定義はありません)。
TypeScript/lib.dom.d.ts at master · microsoft/TypeScript
実装の目標
一例としてinput要素の改造を挙げます。input要素は高頻度で用いて更に改造したい要素です。HTMLInputElementのインターフェースの必要な部分だけ持つコンポーネントを作ることで楽ができます。
input: 入力欄 (フォーム入力) 要素 – HTML: HyperText Markup Language | MDN
HTMLInputElement – Web API | MDN
とりあえずこれはあった方がいい、という機能は次です。
– v-modelで親コンポーネントと値のやり取りができる
– HTMLInputElementで起きたinput, change, blur, focusイベントを親コンポーネントへその名前のまま伝播させられる
これを実現した例は次です。こういったコンポーネント間の伝播がわかりやすいコンポーネントを作る様にするとAtomicDesignの様なコンポーネントの細分化が要求される設計に従った実装もやりやすくなります。
<template>
<label>
<span v-if="$slots.label"><slot name="label" /></span>
<span v-else-if="label">{{ label }}</span>
<!--:value="value"とprops: { value: {type: [String, Number], default: ''},} である要素のv-bind:value="value"を再現-->
<input
:id="inputId"
:type="inputType"
:name="name"
:value="value"
autocomplete="off"
@input="updateValue"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
<!--@input="updateValue"とupdateValue内のイベント通知でv-on:input="value = $event"を再現-->
<!--@hoge="$emit('hoge', $event)"で要素で起きたイベントをそのまま親コンポーネントに通知できる-->
<button
type="button"
@click="changeDisplay"
>{{ inputTypeOptions[inputTypeIndex].nextTypeLabel }}</button>
</label>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
value: {type: [String, Number], default: ''},
label: {type: String, default: null},
name: {type: String, required: true},
inputId: {type: String, default: ''},
},
data() {
return {
inputType: 'password',
inputTypeIndex: 0,
inputTypeOptions: [
{nextTypeLabel: '表示', type: 'password'},
{nextTypeLabel: '非表示', type: 'text'},
],
};
},
methods: {
changeDisplay() {
this.inputTypeIndex++;
if (this.inputTypeIndex >= this.inputTypeOptions.length) {
this.inputTypeIndex = 0;
}
this.inputType = this.inputTypeOptions[this.inputTypeIndex].type;
},
updateValue(event:Event) {
// @input="updateValue"とupdateValue内のイベント通知でv-on:input="value = $event"を再現
if (event.target instanceof HTMLInputElement) {
this.$emit('input', event.target.value);
// 変化後の値をchangeイベントとして通知することにより元のchangeイベントを再現
this.$emit('change', event.target.value);
}
},
},
});
</script>