Vue.jsの入力フォームをコンポネート化(部品化)


vue.jsでは入力フォーム内を構成する各要素をコンポーネント化し再利用することでアプリケーション内で統一したデザインの入力フォームを作成することができます。コンポーネント内の要素に対してCSSを適用するため、コンポーネントを利用する親コンポーネントからCSSを設定する必要がありません。コンポーネントは独立しているため他のプロジェクトで再利用することも可能です。またデザインを変更したい場合は1つのコンポーネントのCSSを変更するだけで変更作業が完了し個別にCSSを適用している場合に比べて作業効率は各段に向上します。
入力フォーム内を構成する最小単位の1つとしてinput要素があります。最近ではAtomic Designを取り入れてアプリケーションを構築することもあり、input要素のコンポーネント化の方法を理解しておくことは非常に重要です。
コンポーネント化の前にVue.jsの入力フォームの利用方法を理解した場合は下記の文書がおすすめです。
目次
v-modelディレクティブとは
input要素などの入力フォームの要素にv-modelディレクティブを設定することで要素に入力した値とVue.jsで定義したデータプロパティで双方向のデータバインディングが行われます。
データバインディングが行われるとinput要素で入力した値がそのままデータプロパティに設定されます。Vue.jsで入力フォームを作成している人にとっては見慣れた書式だと思います。
<input v-model="message" />
//略
//データプロパティのmessageの定義忘れずに
data() {
return {
message: "",
}
}

このv-modelですが実は下記のように書き換えることができます。$event.targetはinput要素そのものです。$event.target.valueによりinput要素に入力した値をvalueで取得しています。
<input v-bind:value="message" v-on:input="message = $event.target.value" />

v-modelを使った書式はsyntax sugar(糖衣構文)と呼ばれ上記のv-bind, v-onを使った記述方法をシンプルでわかりやすくしています。
さらにv-bindとv-onを短縮形にすると以下のようになります。
<input :value="message" @input="message = $event.target.value" />

@inputイベントとは
@inputはinput要素に文字を入力される度に発火されるイベントです。日本語の変換が確定していなくてもイベントは発火されます。文字を入力する度にevent.target.valueの値がmessageプロパティに設定されます。
また以下のように@inputイベントにメソッドを設定して値の設定を行っても処理の動作は変わりません。
<input :value="message" @input="inputValue" />
//略
methods:{
inputValue(){
this.message = event.target.value;
}
}

input要素のコンポーネントの作成
ここまでの説明でv-modelはv-bindと@inputイベントで記述できることがわかりました。その記述を利用することでinput要素のコンポーネント化を行うことができます。
input要素のコンポーネントBaseInput.vueファイルを作成します。input要素をコンポーネント化する目的には”統一されたデザインの入力フォームの作成”があるのでclass(ここではform-inputを設定)を設定しています。それぞれのアプリケーションのデザインに合わせたclassを設定してください。input要素のデザインを変更したい場合はこclassを設定することでこのコンポーネントを利用しているすべてのフォームで変更が反映されます。
Vue 2の場合
設定方法についてはマニュアルのhttps://vuejs.org/v2/guide/components.html#Using-v-model-on-Componentsに記載されています。
<template>
<input
class="form-input"
:value="value"
@input="$emit('input', $event.target.value)"
>
</template>
<script>
export default {
props: ['value'],
}
</script>
BaseInput.vueのコンポーネントは親コンポーネントからpropsでvalueを受け取り、input要素に入力した値は$emitを利用して親コンポーネントにデータを渡しています。$emitを利用して親コンポーネントにデータを渡すというのがポイントになります。

親側のコンポーネントでは@inputを利用して子コンポーネントからemitされたinputイベントを取得してその値をmessageプロパティに設定しています。emitの引数で指定した値は$eventでアクセスすることができます。
<base-input :value="message" @input="message = $event" />

上記の記述は下記のようにsyntax sugar(糖衣構文)であるv-modelを利用して記述することが可能です。
<base-input v-model="message" />
一度コンポーネントを作成すればinputタグからbase-inputタグに変更する必要はありますが、inputタグに対して個別にCSSを適用することなく統一したデザインの入力フォームを作成することができます。
Vue 3の場合
設定方法についてはマニュアルのhttps://v3.vuejs.org/guide/component-basics.html#using-v-model-on-componentsに記載されています。
Vue2の場合は$emitの引数で指定したイベント名がinputでしたがVue3ではupdate:modelValueとなります。Vue3ではイベント名update:modelValueを下記のようにemitsオプションで設定します。イベント名はVue2と異なりますがemitを利用して親コンポーネントに入力した値を渡します。イベント名をupdate:modelValue、propsをmodalValueとして設定しているのはv-modelを利用する場合にそれらの名前がデフォルト値として設定されているためです。
<template>
<input
class="input-form"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script>
export default {
props: ['modelValue'],
emits:['update:modelValue']
}
</script>
v-modelを利用しない場合はpropsはmodel-value、イベント名はupdate:model-valueという名前を設定してください。
<base-input
:model-value="message"
@update:model-value="message = $event"
/>
syntax sugar(糖衣構文)であるv-modelを利用して記述する場合はmodel-valueといった名前はデフォルト値であるため特別それらの名前を指定する必要がありません。
<base-input v-model="message" />
もしpropsのmodelValueというデフォルトの名前を変更したい場合は下記のようにpropsのmodelValueを変更したい名前(ここではtest)に変更します。
<template>
<input
class="input-form"
:value="test"
@input="$emit('update:test', $event.target.value)"
>
</template>
<script>
export default {
props: ['test'],
emits:['update:test']
}
</script>
modelValueからvalueに名前を変更した場合は、親側でv-modelの後ろに変更したpropsの名前のtestをv-model:testのように指定をする必要があります。
<base-input v-model:test="message" />
コンポーネントを利用する場合はinputタグからbase-inputタグへの設定の変更する必要はありますが、inputタグに対して個別にCSSを適用することなくbase-inputコンポーネント内でCSSを適用することで統一したデザインの入力フォームを作成することができます。
$attrsでinputの属性を渡す
input要素のtype, name, placeholder属性をBaseInputコンポーネントに渡したい時はインスタンスプロパティの$attrsを利用することができます。
texxt-inputタグにname, type, placeholder属性を設定します。$attrsについてはVue3, Vue2でも同じように利用することができます。
<base-input v-model="message" name="message" type="text" placeholder="メッセージを入力してください" />
$attrsから本当に設定した属性が取得できるのか確認を行います。
<template>
{{ $attrs }}
<input
class="input-form"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<script>
export default {
props: ['modelValue'],
emits:['update:modelValue']
}
</script>
ブラウザ上にはbase-iputタグで設定した属性が表示されます。
{ "type": "text", "name": "message", "placeholder": "メッセージを入力してください" }
これらの属性をBaseInput.vueコンポーネントのinput要素に設定するためにv-bindを利用します。v-bindを利用するとすべてのpropsを一度に親コンポーネントから子コンポーネントに渡すことができます。
<input
class="input-form"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
v-bind="$attrs"
>
input要素のname属性にmessage, type属性にtext, placeholderにメッセージを入力してくださいと設定されたinputが表示されます。

v-bindを利用して下記のようにコードを記述することもできます。
<template>
<input
class="input-form"
:value="modelValue"
v-bind="{
...$attrs,
onInput : ($event) => $emit('update:modelValue', $event.target.value)
}"
>
</template>
select要素のコンポーネントの作成
select要素もinput要素のようにコンポーネント化することが可能です。select要素の場合はinput要素の場合と異なり、selectで選択するoptionをpropsで渡す必要があります。その他の設定についてはinput要素のコンポーネント化の方法が理解できれば難しいことは何もありません。
select要素のコンポーネント化の前にコンポーネント化なしの状態でv-modelを利用し動作確認を行います。選択するオプションにはcarsの配列を準備し、v-forを利用して展開しています。
<template>
<div>
<select v-model="car" >
<option
v-for="car in cars"
:key="car"
:value="car"
>
{{car}}
</option>
</select>
<p>{{ car }}</p>
</div>
</template>
<script>
export default {
components:{
},
data() {
return {
car:'audi',
cars: ['volvo','saab','mercedes','audi'],
}
},
}
</script>
選択したオプションが表示されるように{{ car }}でブラウザ上に表示させています。select要素で項目を選択すると選択した車が表示されます。

Vue 2の場合
select要素をコンポーネント化するためBaseSelect.vueファイルを作成します。propsを使って親コンポーネントからselect要素の選択肢のoptionsと選択した値のvalueを受け取ります。選択肢を変更するとchangeイベントが発火するので$emitを利用してイベント名inputで選択した値を親コンポーネントに渡します。親コンポーネントでv-modelを利用する場合は、イベント名はinputに設定する必要があります。
<template>
<select
:value="value"
@change="$emit('input',$event.target.value)"
>
<option
v-for="(option,index) in options"
:key="index"
:selected="option === value">
{{ option }}
</option>
</select>
</template>
<script>
export default {
props: ['options','value'],
}
</script>
親側ではpropsを利用してデータプロパティのcarsをBaseSelectコンポーネントに渡します。v-modelには選択肢の値であるcarを設定します。
<base-select
:options="cars"
v-model="car"
/>
//略
import BaseSelect from './components/BaseSelect.vue'
export default {
name: 'App',
components: {
BaseSelect,
},
data(){
return {
car:'audi',
cars: ['volvo','saab','mercedes','audi'],
}
}
}
v-modelを利用しない場合はpropsでoptionsだけではなくcarも渡します。子コンポーネントの$emitの引数に指定したいるイベント名のinputを利用して子コンポーネントから値を受け取ります。
<base-select
:options="cars"
:value="car"
@input="car = $event"
/>
Vue 3の場合
inputの場合と同様にイベント名はupdate:modelValueとなります。propsの名前も同様にinputと同じmodelValueとします。v-modelを利用する場合はmodelValueという名前の設定は必須となりますがv-modelを使用しないば場合は任意の名前をつけることが可能です。
<template>
<select
:value="modelValue"
@change="$emit('update:modelValue',$event.target.value)"
>
<option
v-for="(option,index) in options"
:key="index"
:selected="option === modelValue">
{{ option }}
</option>
</select>
</template>
<script>
export default {
props: ['options','modelValue'],
}
</script>
親コンポーネントでは、v-modelを使って以下のように記述することができます。
<base-select :options="cars" v-model="car" />
v-modelを利用せず下記のように記述することも可能です。
<base-select
:options="cars"
:model-value="car"
@update:model-value="car = $event"
/>
radioボタンのコンポーネントの作成
radio要素もinput要素のようにコンポーネント化することが可能です。
radio要素のコンポーネント化の前にコンポーネント化なしの状態でv-modelを利用し動作確認を行います。選択可能なオプションにはcarsの配列を準備し、v-forを利用して展開しています。radioボタンではinput要素のtypeをradioに設定します。
<template>
<div>
<div v-for="(car,index) in cars" :key="index">
<input
type="radio"
name="car_selection"
v-model="favorite_car"
:value="car.value"
/>{{ car.label }}
</div>
<div>
{{ favorite_car }}
</div>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
favorite_car:'mercedes',
cars: [
{label:'volvo',value:'volvo'},
{label:'saab',value:'saab'},
{label:'mercedes',value:'mercedes'},
{label:'audi',value:'audi'}
]
}
}
}
</script>
favorite_carにmercedsを設定しているでページを開くとmercedsを選択した状態になっています。favorite_carの設定値を変更することで最初に選択されている車を変更することができます。もし最初の設定を行いたくない場合は空白を設定してください。
その他の車を選択すると選択した車の名前がラジオボタンの下に表示されます。

vue 2の場合
radio要素をコンポーネント化するためにBaseRadio.vueファイルを作成します。propsを利用して、ラジオボタンの選択肢であるoptionsをpropsで受け取り、v-forで展開します。選択肢を変更した場合はchangeイベントが発火してイベント名inputとして選択した値を親コンポーネントに渡します。
<template>
<div>
<div v-for="(option,index) in options" :key="index">
<input
type="radio"
:name="name"
:value="option.value"
:checked="option.value === value"
@change="$emit('input',$event.target.value)"
/>{{ option.label }}
</div>
</div>
</template>
<script>
export default {
props: ['options','name','value'],
}
</script>

親コンポーネントではv-modelでデータプロパティfavorite_carを設定し、propsのoptionsにはcarsを設定しています。name属性の値であるselection_carもpropsで渡しています。
<template>
<div>
<base-radio :options="cars" name="car_selection" v-model="favorite_car" />
<div>
{{ favorite_car }}
</div>
</div>
</template>
<script>
import BaseRadio from "./components/BaseRadio"
export default {
components:{
BaseRadio
},
data(){
return {
favorite_car:'mercedes',
cars: [
{label:'volvo',value:'volvo'},
{label:'saab',value:'saab'},
{label:'mercedes',value:'mercedes'},
{label:'audi',value:'audi'}
]
}
}
}
</script>
コンポーネント化前と同様にラジオボタンの変更すると選択した車の名前が表示されます。
v-modelを利用しない場合は下記のように記述することができます。
<base-radio
:options="cars"
name="car_selection"
:value="favorite_car"
@input="favorite_car = $event"
/>
Vue 3の場合
これまでのinput, select要素と同様にpropsにmodelValue、イベント名にupdate:modelValueを設定します。
<template>
<div v-for="(option,index) in options" :key="index">
<input
type="radio"
:name="name"
:value="option.value"
:checked="option.value === modelValue"
@change="$emit('update:modelValue',$event.target.value)"
/>{{ option.label }}
</div>
</template>
<script>
export default {
props: ['options','name','modelValue'],
}
</script>
親コンポーネントではVue 2の場合と同様にv-modelとpropsにcarsとnameを設定します。
<base-radio :options="cars" name="car_selection" v-model="favorite_car" />
v-modelを利用しない場合は下記のように記述することも可能です。
<base-radio
:options="cars"
name="car_selection"
:model-value="favorite_car"
@update:model-value="favorite_car = $event"
/>
ラジオボタン要素のinputをコンポーネント化
ここまでの説明でラジオボタンをBaseRadio.vueとしてコンポーネント化することができましたがBaseRadio.vueファイル中でv-forを展開しているためinput要素がBaseRadio.vueの中に複数存在することになります。
ここではさらにinput要素をコンポーネント化します。ファイル名をBaseInputRadio.vueとします。先ほど作成したBaseRadio.vueファイルも利用しますが更新が必要となります。
BaseInputRadioコンポーネントの親コンポーネントに当たるBaseRadio.vueからpropsとしてname属性の値、label, value(labelとvalueはinput毎に異なる値を持つ), modelValueが渡されます。ラジオボタンが選択された場合にchangeイベントでupdate:modelValueという名前で親コンポーネントBaseRadio.vueに値を渡します。
<template>
<input
type="radio"
:name="name"
:value="value"
:checked="value === modelValue"
@change="$emit('update:modelValue',$event.target.value)"
/>{{ label }}
</template>
<script>
export default {
props: ['label','name','modelValue','value'],
}
</script>
BaseRadio.vueファイルでは新たに作成したBaseInputRadio.vueファイルをimportします。BaseRadioコンポーネントはupdate:modelValueイベントを受け取り、さらにupdate:modelValueでそのまま親コンポーネントに値を渡します。v-forで展開し、optionsのlabelとvalueの値を各BaseInputRadioコンポーネントにpropsで渡します。
<template>
<base-input-radio
v-for="(option,index) in options"
:key="index"
:label="option.label"
:value="option.value"
:modelValue="modelValue"
:name="name"
@update:modelValue="$emit('update:modelValue', $event)"
/>
</template>
<script>
import BaseInputRadio from "./BaseInputRadio"
export default {
components:{
BaseInputRadio
},
props: ['options','name','modelValue'],
}
</script>
親コンポーネントには変更はなく先ほど作成したコードをそのまま利用できます。
<base-radio :options="cars" name="car_selection" v-model="favorite_car" />
input要素(type=”radio”)をコンポーネント化してもラジオボタンとして利用できることが確認できました。
つづく