フォームを構成するinput要素などのコンポーネント化はVueのバージョンが2なのか3なのかまたOptions APIかComposition APIかによってコンポーネントの設定方法が異なるため複数の方法で記述しています。またTypScriptを利用するかどうかで記述方法が変わります。利用予定のバージョンと記述方法を確認して参考にしてみてください。
fukidashi

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>
input要素の表示
input要素の表示

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に保存しています。

$eventという名前の変数なので$を省略するとエラーとなります。
fukidashi

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)" />
v-modelから:valueと@inputを使って記述に変更しても動作が変わらないことを確認してください。
fukidashi

@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要素に入力した値)の値を渡しています。

Vue.jsでは親コンポーネントから子にデータを渡したい場合はprops、子コンポーネントから親にデータを渡したい場合は$emitを利用します。
fukidashi

親側のコンポーネントでは@inputを利用して子コンポーネントからemitによるinputイベントを受け取りその値をmessageプロパティに設定しています。BaseInputコンポーネントでemitの引数に指定した値は$eventでアクセスすることができます。


<base-input :value="message" @input="message = $event" />
emitで設定しているinputという名前は任意の名前なので別の名前にしても動作します。その場合は親側で受け取るイベント名も忘れずに変更を行ってください。
fukidashi

上記の記述は下記のように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" />
modelValueをmodel-Valueとして記述しても動作します。
fukidashi

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が表示されます。

$attrsを設定後に反映されることを確認
$attrsを設定後に反映されることを確認

ブラウザでのデベロッパーツールの要素でinput要素に設定されている属性も確認しておきます。

attrsによるinput要素の属性の確認
attrsによる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が設定されていることがわかります。

div要素に設定されたclassとplaceholder
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要素で項目を選択すると選択した車が表示されます。

select要素が表示
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の設定値を変更することで最初に選択されている車を変更することができます。もし最初の設定を行いたくない場合は空白を設定してください。

その他の車を選択すると選択した車の名前がラジオボタンの下に表示されます。

saabを選択した場合
saabを選択した場合

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>
:checked=”option.value===value”を削除するとページを表示した時にコンポーネントで設定していた初期値のfavorite_carの選択が行われません。(初期値がチェックされない)削除した状態でも選択を変更すれば選択した車は表示されます。
fukidashi

親コンポーネントでは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”)をコンポーネント化してもラジオボタンとして利用できることが確認できました。