Vue.jsの入力フォームをコンポネート化(部品化)
Vue.jsでは入力フォーム内を構成する各要素(input, selectなど)をコンポーネント化し、コンポーネント内部でスタイルを設定し再利用することでアプリケーション内で統一したデザインの入力フォームを作成することができます。コンポーネント内部でスタイルを適用することで親コンポーネントからCSSを設定する必要がなくなり要素毎に同じスタイルを適用する手間が省けます(propsやFallthrough Attributesによって親コンポーネントからスタイルを設定することも可能です)。さらにコンポーネント化して独立させることで1つのプロジェクトだけではなく複数のプロジェクトで再利用することも可能です。またデザインを変更したい場合は1つのコンポーネントのCSSを変更するだけで変更作業が完了し個別にCSSを適用している場合に比べて格段に作業効率が向上します。
入力フォーム内を構成する最小単位の1つとしてinput要素があります。最近ではAtomic Designを取り入れてアプリケーションを構築することもあり、input要素のコンポーネント化の方法を理解しておくことは非常に重要です。
コンポーネント化の前にVue.jsの入力フォームの利用方法を理解したい場合は下記の文書がおすすめです。Options APIとComposition APIを利用した設定方法を記述しています。
v-modelディレクティブとは
input要素などの入力フォームの要素にv-modelディレクティブを設定することで要素に入力した値とVue.jsで定義したデータプロパティで双方向のデータバインディングが行われます。
双方向のデータバインディングが行われるとinput要素で入力した値がそのままデータプロパティに設定され、データプロティの値を更新するとinput要素の文字列が更新されます。つまりinput要素内のデータとJavaScript上で定義したデータが同期します。
下記がOptions APIを記述した場合のv-modelの設定です。
Options APIでの記述方法
<script>
export default {
data() {
return {
message: '',
};
},
};
</script>
<template>
<input v-model="message" />
</template>
Composition APIでの記述方法
Composition APIではref関数を利用してmessageを定義します。
<script setup>
import { ref } from 'vue';
const message = ref('');
</script>
<template>
<input v-model="message" />
</template>
v-modelを分解
input要素に設定したv-modelですが実は下記のように書き換えることができます。
<input v-bind:value="message" v-on:input="message = $event.target.value" />
$event.targetはinput要素そのものを参照しています。そのため$event.target.valueからinput要素に入力した値を取得できます。文字を入力する度に発火するinputイベントを利用することでinput要素に入力した値を$event.target.valueから取得してmessageに保存しています。
v-modelを使った書式はsyntax sugar(糖衣構文)と呼ばれ、v-bind, v-onを使った記述方法をシンプルでわかりやすくしています。
さらにv-bindとv-onを短縮形にすると以下のようになります。
<input :value="message" @input="message = $event.target.value" />
関数を利用して下記のように記述することもできます。
<input :value="message" @input="(event) => (message = event.target.value)" />
@inputイベントとは
@inputはinput要素に文字を入力される度に発火されるイベントです。日本語の変換が確定していなくてもイベントは発火されます。文字を入力する度に$event.target.valueの値がmessageプロパティに設定されます。
また以下のように@inputイベントにメソッドを設定して値の設定を行っても処理の動作は変わりません。
【Options APIの場合】
<template>
<input :value="message" @input="inputValue" />
</template>
<script>
export default {
data() {
return {
message: '',
};
},
methods: {
inputValue(event) {
this.message = event.target.value;
},
},
};
</script>
【Composition APIの場合】
ref関数で定義したreactiveな変数に対して値を設定する場合はvalueが必要です。Options APIのようにthisは使えません。
<script setup>
import { ref } from 'vue';
const message = ref('');
const inputValue = (event) => {
message.value = event.target.value;
};
</script>
<template>
<input :value="message" @input="inputValue" />
</template>
input要素のコンポーネントの作成
ここまでの説明でv-modelはv-bindと@inputイベントで記述できることがわかりました。その記述を利用することでinput要素のコンポーネント化を行うことができます。
プロジェクトのsrcフォルダの下にcomponentsフォルダを作成して、input要素を設定するためコンポーネント用ファイルBaseInput.vueを作成します。input要素をコンポーネント化する目的の一つには”統一されたデザインの入力フォームの作成”があるのでclassにform-inputを設定しています。
<input
class="form-input"
:value="value"
@input="$emit('input', $event.target.value)"
/>
それぞれのアプリケーションのデザインに合わせたclassを設定してください。ここではform-inputを設定していますがclassでのスタイルの設定は行いません。input要素のデザインを変更したい場合はこのclassに設定したform-inputのスタイルを変更することでBaseInputコンポーネントを利用しているすべてのフォームで変更が反映されます。
Vue 2の場合
input要素のコンポーネント化について設定方法についてはマニュアルのhttps://v2.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を利用したinputイベントで親コンポーネントに$event.target.value(input要素に入力した値)の値を渡しています。
親側のコンポーネントでは@inputを利用して子コンポーネントからemitによるinputイベントを受け取りその値をmessageプロパティに設定しています。BaseInputコンポーネントでemitの引数に指定した値は$eventでアクセスすることができます。
<base-input :value="message" @input="message = $event" />
上記の記述は下記のようにsyntax sugar(糖衣構文)であるv-modelを利用して記述することが可能です。
<base-input v-model="message" />
入力フォームではinputタグから新たに作成したbase-inputタグに変更する必要はありますが、BaseInputコンポーネントでは内部でスタイルを設定している(例:form-inputクラス)ためinputタグに対して個別にCSSを適用する必要がありません。入力フォームの中で複数のBaseInputコンポーネントを利用することで統一したデザインの入力フォームを作成することができます。例えば下記のようにフォームの中で複数のBaseInputコンポーネントを利用することができます。
<base-input :value="firstName" @input="firstName = $event" />
<base-input :value="lastName" @input="lastName = $event" />
<base-input :value="postalCode" @input="postalCode = $event" />
// or
<base-input v-model="firstName" />
<base-input v-model="lastName" />
<base-input v-model="postalCode" />
Vue 3の場合
Vue3での設定方法についてはマニュアルのhttps://vuejs.org/guide/components/events.html#usage-with-v-modelを参考に行っています。
Options APIでの記述方法
Vue2の場合は$emitの引数で指定したイベント名がinputでしたがVue3ではupdate:modelValueとなります。Vue3ではtemplate内で$emitを利用する場合は必須ではありませんがイベント名update:modelValueを下記のようにemitsオプションで明示的に指定することもできます。
<template>
<input
class="input-form"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
};
</script>
$emitsで指定するイベント名はVue2と異なりますがemitを利用したイベントで親コンポーネントに入力した値を渡します。親コンポーネントから利用する場合は下記のように記述します。
<base-input :modelValue="message" @update:modelValue="message = $event" />
syntax sugar(糖衣構文)であるv-modelを利用して記述することができます。
<base-input v-model="message" />
イベント名をupdate:modelValue、propsをmodalValueとして設定しているのはv-modelを利用する場合にそれらの名前がデフォルト値として設定されているためです。
もし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" />
Composition APIでの記述方法
Composition APIではpropsはdefinePropsを利用します。emitsは明示的にdefineEmitsで定義することもできますが必須ではありません。
<script setup>
defineProps({
modelValue: String,
});
defineEmits(['update:modelValue']);
</script>
<template>
<input
class="input-form"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
上記のコードではdefineEmitsを明示的に記述しただけでしたが戻り値を利用することで$emitとemitに変更して記述することができます。
<script setup>
defineProps({
modelValue: String,
});
const emit = defineEmits(['update:modelValue']);
</script>
<template>
<input
class="input-form"
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
v-modelを利用しない場合はpropsはmodelValue、イベント名はupdate:modelValueという名前を設定してください。
<base-input :modelValue="message" @update:modelValue="message = $event" />
syntax sugar(糖衣構文)であるv-modelを利用して記述する場合はmodelValueといった名前はデフォルト値であるため特別それらの名前を指定する必要がありません。
<base-input v-model="message" />
コンポーネントを利用する場合はinputタグからbase-inputタグへの設定の変更する必要はありますが、inputタグに対して個別にCSSを適用することなくbase-inputコンポーネント内でCSSを適用することで統一したデザインの入力フォームを作成することができます。
VueUseのuseVModelの利用
BaseInput.vueファイルのinput要素への下記のコードをシンプルにするためにVueUseのuseVModelを利用することができます。
VueUseを利用するためにプロジェクトで@vueuse/coreをインストールしておく必要があります。
% npm i @vueuse/core
インストール完了後、@vueuse/coreからimportしたuseVModelを利用して下記のように書き換えることができます。
<script setup>
import { useVModel } from '@vueuse/core';
const props = defineProps({
modelValue: String,
});
const emit = defineEmits(['update:modelValue']);
const modelValue = useVModel(props, 'modelValue', emit);
</script>
<template>
<input class="input-form" v-model="modelValue" />
</template>
useVModelの利用はshadcn-vueライブラリのInputコンポーネントの中でも利用されています。コードは下記の通りです。
<script setup>
import { useVModel } from "@vueuse/core";
import { cn } from "@/lib/utils";
const props = defineProps({
defaultValue: { type: [String, Number], required: false },
modelValue: { type: [String, Number], required: false },
class: { type: null, required: false },
});
const emits = defineEmits(["update:modelValue"]);
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
});
</script>
<template>
<input
v-model="modelValue"
:class="
cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"
/>
</template>
TypeScriptでの記述
Compositions APIで記述したコードをTypeScriptのコードに書き換えると以下のように記述することができます。scrtiptタグにはTypeScriptを利用しているためlangでtsを設定しています。
<script setup lang="ts">
defineProps<{
modelValue: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
</script>
<template>
<input
class="input-form"
:value="modelValue"
@input="
emit('update:modelValue', ($event.target as HTMLInputElement).value)
"
/>
</template>
$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から本当に設定した属性が取得できるのか確認を行います。
【Options APIの場合】
<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>
【Composition APIの場合】
<script setup>
defineProps({
modelValue: String,
});
const emit = defineEmits(['update:modelValue']);
</script>
<template>
{{ $attrs }}
<input
class="input-form"
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
ブラウザ上には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が表示されます。
ブラウザでのデベロッパーツールの要素でinput要素に設定されている属性も確認しておきます。
v-bindを利用して下記のようにコードを記述することもできます。
<template>
<input
class="input-form"
:value="modelValue"
v-bind="{
...$attrs,
onInput : ($event) => $emit('update:modelValue', $event.target.value)
}"
>
</template>
useAttrs関数
useAttrs関数を利用することでscriptタグ内からattrsの値を確認することもできます。
<script setup>
import { useAttrs } from 'vue';
defineProps({
modelValue: String,
});
const emit = defineEmits(['update:modelValue']);
const attrs = useAttrs();
console.log(attrs);
</script>
Fallthrough Attributes
$attrsを利用してtype, name, placeholderをinput要素に設定することができましたがinput要素がルート要素に設定されている場合はFallthrough Attributesの機能を利用して$attrsを利用することなく属性を設定することができます。
input要素がルート要素なのでdivなどの要素に囲まれていないことがポイントです。
<template>
<input
class="input-form"
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
親コンポーネントからclassとplaceholderを渡します。
<template>
<BaseInput
v-model="message"
class="another-class"
placeholder="メッセージを入力してください"
/>
<p>{{ message }}</p>
</template>
デベロッパーツールで要素を確認するとclassが追加され、placeholderが設定されていることがわかります。
input要素をdiv要素で囲みます。これでinput要素はルート要素ではなくなります。
<template>
<div>
<input
class="input-form"
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</div>
</template>
再度デベロッパーツールで要素を確認するとdiv要素にclassでplaceholderが設定されていることがわかります。
select要素のコンポーネントの作成
select要素もinput要素のようにコンポーネント化することが可能です。select要素をコンポーネント化する場合は、select要素の場合は選択するoptionをpropsで渡す必要があります。その他の設定についてはinput要素のコンポーネント化の方法が理解できれば難しいことは何もありません。
コンポーネント化なし
復習となりますがコンポーネント化しない場合の設定方法を確認しておきます。
Vue2の場合
select要素のコンポーネント化なしの状態でv-modelを利用し動作確認を行います。選択するオプションにはcarsの配列を準備し、v-forを利用して展開しています。Vue2で記述した場合です。
<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 {
data() {
return {
car:'audi',
cars: ['volvo','saab','mercedes','audi'],
}
},
}
</script>
選択したオプションが表示されるように{{ car }}でブラウザ上に表示させています。select要素で項目を選択すると選択した車が表示されます。
Vue3(Composition API)
Vue3でコンポーネント化していないselect要素を設定する場合は下記となります。
<script setup>
import { ref } from 'vue';
const car = ref('audi');
const cars = ['volvo', 'saab', 'mercedes', 'audi'];
</script>
<template>
<select v-model="car">
<option v-for="car in cars" :key="car" :value="car">
{{ car }}
</option>
</select>
<p>{{ car }}</p>
</template>
Vue 2の場合
select要素をコンポーネント化するためcomponentsフォルダに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の場合
Options APIでの記述方法
inputの場合と同様にイベント名はupdate:modelValueとなります。propsの名前も同様にinputと同じmodelValueとします。親コンポーネントでv-modelを利用する場合はmodelValueという名前の設定は必須となりますがv-modelを使用しないば場合は任意の名前をつけることが可能です。(設定方法についてinput要素を参照してください。)
<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"
:modelValue="car"
@update:modelValue="car = $event"
/>
Composition APIでの記述方法
Composition APIではpropsはdefinePropsを利用します。emitはdefineEmitsの戻り値を利用せず$emitと設定することもで可能ですが、ここではdefineEmitsの戻り値emit関数を利用しています。
<script setup>
defineProps({
modelValue: String,
options: Array,
});
const emit = defineEmits(['update:modelValue']);
</script>
<template>
<select
:value="modelValue"
@change="emit('update:modelValue', $event.target.value)"
>
<option
v-for="option in options"
:key="option"
:selected="option === modelValue"
>
{{ option }}
</option>
</select>
</template>
親コンポーネントからは以下のように利用します。
<script setup>
import { ref } from 'vue';
import BaseSelect from './components/BaseSelect.vue';
const car = ref('audi');
const cars = ['volvo', 'saab', 'mercedes', 'audi'];
</script>
<template>
<base-select :options="cars" v-model="car" />
{{ car }}
</template>
TypeScriptでの記述
TypeScriptでは下記のように記述することができます。
<script setup lang="ts">
defineProps<{
modelValue: string;
options: string[];
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
</script>
<template>
<select
:value="modelValue"
@change="
emit('update:modelValue', ($event.target as HTMLSelectElement).value)
"
>
<option
v-for="option in options"
:key="option"
:selected="option === modelValue"
>
{{ option }}
</option>
</select>
</template>
radioボタンのコンポーネントの作成
radio要素のコンポーネント化の設定を確認していきます。radio要素を作成する場合はinput要素のtype属性をradioに設定します。
コンポーネント化なし
復習となりますがコンポーネント化しない場合の設定方法を確認しておきます。
Vue2の場合
radio要素で選択可能なオプションには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の設定値を変更することで最初に選択されている車を変更することができます。もし最初の設定を行いたくない場合は空白を設定してください。
その他の車を選択すると選択した車の名前がラジオボタンの下に表示されます。
Vue3(Composition API)
Vue3でコンポーネント化していないradio要素をComposition APIで設定する場合は下記となります。
<script setup>
import { ref } from 'vue';
const favorite_car = ref('mercedes');
const cars = [
{ label: 'volvo', value: 'volvo' },
{ label: 'saab', value: 'saab' },
{ label: 'mercedes', value: 'mercedes' },
{ label: 'audi', value: 'audi' },
];
</script>
<template>
<div v-for="(car, index) in cars" :key="index">
<input
type="radio"
name="car_selection"
v-model="favorite_car"
:value="car.value"
/><label :for="car.label">{{ car.label }}</label>
</div>
<div>
{{ favorite_car }}
</div>
</template>
labelとvalueが同じ場合は下記のように記述することもできます。
<script setup lang="ts">
import { ref } from 'vue';
const favorite_car = ref('mercedes');
const cars = ['volvo', 'saab', 'mercedes', 'audi'];
</script>
<template>
<div v-for="car in cars" :key="car">
<input
type="radio"
name="car_selection"
v-model="favorite_car"
:value="car"
/><label :for="car">{{ car }}</label>
</div>
<div>
{{ favorite_car }}
</div>
</template>
vue 2の場合
radio要素をコンポーネント化するためにcomponentsフォルダに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の場合
Options APIの場合
これまでの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"
/>
Composions APIの場合
Composition APIではpropsはdefinePropsを利用します。emitはdefineEmitsの戻り値を利用せず$emitと設定することもで可能ですが、ここではdefineEmitsの戻り値emit関数を利用しています。
<script setup>
defineProps({
modelValue: String,
name: String,
options: Array,
});
const emit = defineEmits(['update:modelValue']);
</script>
<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)"
/><label :for="option.label">{{ option.label }}</label>
</div>
</template>
親コンポーネントからは以下のように利用します。
<script setup>
import { ref } from 'vue';
import BaseRadio from './components/BaseRadio.vue';
const favorite_car = ref('mercedes');
const cars = [
{ label: 'volvo', value: 'volvo' },
{ label: 'saab', value: 'saab' },
{ label: 'mercedes', value: 'mercedes' },
{ label: 'audi', value: 'audi' },
];
</script>
<template>
<base-radio :options="cars" v-model="favorite_car" name="car_selection" />
{{ favorite_car }}
</template>
TypeScriptでの記述
TypeScriptでは下記のように記述することができます。
<script setup lang="ts">
defineProps<{
modelValue: string;
name: string;
options: [
{
label: string;
value: string;
}
];
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
</script>
<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 as HTMLInputElement).value)
"
/><label :for="option.label">{{ option.label }}</label>
</div>
</template>
ラジオボタン要素の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”)をコンポーネント化してもラジオボタンとして利用できることが確認できました。