Vue.jsのカスタムコンポーネントを拡張する
既存のコンポーネントの機能を生かしつつ、機能追加する方法のまとめ。
VuetifyのVTextField
を拡張して、数値をカンマ区切りで表示する入力フィールドを作ります。
仕様
- 入力(focus)時は
type="number"
としてカンマなしの数値を入力する - 表示(非focus)時は
type="text"
として3桁のカンマ区切りで表示する - valueの型は
Number|null
とする -
v-model
での双方向バインディングが出来るようにする
ラッパーコンポーネントの作成
まずプロパティ、イベント、スロットを透過的にVTextField
に渡すコンポーネントを作成します。
プロパティ
プロパティをVTextField
に受け渡すために属性の継承を無効化して、v-bind
を指定します。
デフォルトの挙動はテンプレートのルート要素に属性が継承されますが、$props
に受け渡されるわけではなく、$attrs
に直接渡されてしまうため、期待した動作をしません。
※この辺りの詳細は参考の記事に分かりやすくまとめられています
また、VTextField
をルート以外の要素にしたい場合はデフォルトの継承では出来ないのでv-bind
で渡す必要があります。
<template>
<v-text-field
v-bind="$attrs"
>
</v-text-field>
</template>
<script>
export default {
inheritAttrs: false,
}
</script>
参考
イベント
v-on
にイベントリスナーを受け渡します。
後のv-model
の実装を分かりやすくするため:value
props
listeners()
を実装していますが、子がカスタムコンポーネントなので、inputイベントの内容を変更する必要が無ければv-on="$listeners"
を追加するだけでも動作します。
<template>
<v-text-field
v-bind="$attrs"
:value="value"
v-on="listeners"
>
</v-text-field>
</template>
<script>
export default {
inheritAttrs: false,
props: ['value'],
computed: {
listeners() {
const vm = this;
return {
...vm.$listenres,
// 子がカスタムイベントなのでeventに直接値が入っている
input: event => vm.$emit('input', event)
};
},
}
}
</script>
参考
スロット
$slotsからスロット名を取り出して、VTextField
に渡します。
<template>
<v-text-field
>
<template v-for="(value, name) in $slots" v-slot:[name]>
<slot :name="name"/>
</template>
</v-text-field>
</template>
参考
これで作成したコンポーネントがVTextField
の機能をすべて利用できるようになりました。
実装
続いて追加機能を実装します。
入力時はtype="number"
、表示時はtype="text"
にする。
dataに入力/表示の状態を保持するediting
を追加して、@focusin
@focusout
で制御します。
<template>
<v-text-field
:type="type"
@focusin="edit"
@focusout="display"
>
</v-text-field>
</template>
<script>
export default {
data() {
return {
editing: false
};
},
computed: {
type() {
if (this.editing) {
return "number";
} else {
return "text";
}
}
},
methods: {
display() {
this.editing = false;
},
edit() {
this.editing = true;
}
}
};
</script>
valueの型をNumberに。入力時はカンマ無し、表示時はカンマ区切り。
valueをNumberにして、このコンポーネントが受け取る場合はNumber、VTextField
に渡す場合はstringにします。
受け渡し用に算出プロパティのinnerValue
を追加して、getterでNumber -> stringとカンマ区切りの変換、setterでstring -> Number の変換を行います。
算出プロパティを使うことでその中で使用している変数(value、editing)が変更された場合に算出プロパティに反映されます。
<template>
<v-text-field
v-bind="$attrs"
:value="innerValue" // VTextFieldには算出プロパティを渡す
v-on="listeners"
>
</v-text-field>
</template>
<script>
export default {
inheritAttrs: false,
props: {
value: {
type: Number,
default: null
}
},
data() {
return {
editing: false
};
},
computed: {
listeners() {
const vm = this;
return {
...vm.$listenres,
input: event => vm.innerValue = event // 直接emitせずにinnerValueを通すように変更
};
},
innerValue: {
get() {
if (this.editing) {
return this.value != null ? this.value.toString() : "";
} else {
return this.value != null ? this.value.toLocaleString() : "";
}
},
set(newValue) {
const value = newValue.length > 0 ? Number(newValue) : null;
this.$emit("input", value);
}
},
},
};
</script>
これで、v-model
に値を渡せば双方向バインディングでリアルタイムに書き換わるようになります。
呼び出し元コンポーネント
<template>
<div id="app">
<my-number-field v-model="val" label="Number Field">
<template v-slot:append>円</template>
</my-number-field>
</div>
</template>
<script>
import MyNumberField from "./components/MyNumberField.vue";
export default {
name: "App",
components: {
MyNumberField
},
data() {
return {
val: 1000
};
},
}
</script>
コード
全体コードはこちら。