# Composition APIでTypeScriptを使いv-modelで扱える簡易なフォームコンポーネントを作る
Composition APIでTypeScriptを使って、フォームを作るサンプルコードを書いた。
フォームのコンポーネントとしてinput[type=text]、textarea、checkbox、radio、selectの簡易なものを用意した。
フォームのコンポーネントは親コンポーネントと子コンポーネント間のデータのやりとりがハマりどころだ。だからv-model
が使えることを確認した。
コンポーネント間でデータのやり取りが出来てさえしまえば後は項目を追加するだけなので、idやname、disabled、placeholderなどその他の項目は書いていない。
親コンポーネントから以下のように使えるコンポーネントを作っていく。
NuxtJSで試しているけど、vuejs/composition-api
でも変わらないと思う。
<template>
<div>
<div>
{{ form }}
</div>
<MyInput v-model="form.text" />
<MyTextarea v-model="form.longText" />
<MyCheckbox v-model="form.checked">check</MyCheckbox>
<MyRadio v-model="form.picked" label="one">One</MyRadio>
<MyRadio v-model="form.picked" label="two">Two</MyRadio>
<MySelect v-model="form.selected" :options="options" />
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from '@nuxtjs/composition-api'
interface State {
text: string
longText: string
checked: boolean
picked: string
selected: string
}
export default defineComponent({
setup() {
const form = reactive<State>({
text: 'init text',
longText: 'init long text',
checked: false,
picked: 'two',
selected: 'b',
})
const options = [
{ label: 'A', value: 'a' },
{ label: 'B', value: 'b' },
{ label: 'C', value: 'c' },
]
return {
form,
options,
}
},
})
</script>
# Input
inputタグでtype="text"の場合、props名value
で値を渡し、input
イベントで親コンポーネントへ変更を知らせる。
event.target
はHTMLInputElement
として扱うことでvalue
を取得できる。
<template>
<input :value="value" type="text" @input="handleInput" />
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
props: {
value: {
type: [String, Number],
default: '',
},
},
emits: ['input'],
setup(_, ctx) {
const handleInput = (e: Event) => {
const target = e.target as HTMLInputElement
ctx.emit('input', target.value)
}
return {
handleInput,
}
},
})
</script>
# Textarea
textareaタグはinputタグでtype="text"と同様に、props名value
で値を渡し、input
イベントで親コンポーネントへ変更を知らせる。
event.target
はHTMLTextAreaElement
として扱うことでvalue
を取得できる。
<template>
<textarea :value="value" @input="handleInput"></textarea>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
props: {
value: {
type: [String, Number],
default: '',
},
},
emits: ['input'],
setup(_, ctx) {
const handleInput = (e: Event) => {
const target = e.target as HTMLTextAreaElement
ctx.emit('input', target.value)
}
return {
handleInput,
}
},
})
</script>
# Checkbox
inputタグでtype="checkbox"の場合、props名checked
で値を渡し、change
イベントで親コンポーネントへ変更を知らせるようにした。
event.target
はHTMLInputElement
として扱うことでvalue
を取得できる。
自作のコンポーネントは、デフォルトではprops名はvalue
、親コンポーネントへinput
イベントで変更を知らせようとする。
デフォルトではコンポーネントにある v-model は value をプロパティとして、input をイベントして使います https://jp.vuejs.org/v2/guide/components-custom-events.html#v-model-を使ったコンポーネントのカスタマイズ (opens new window)
props名と親コンポーネントへ変更を知らせるイベントを変更したい場合、model
オプションを使う。
model: {
prop: 'checked',
event: 'change',
},
イベント名は変更したほうがハマりづらいのかなと思う。イベント名を変更しない場合、子コンポーネントで@change
を使っているにも関わらず、親コンポーネントでは@input
でイベントを受けないといけないからだ。
<template>
<label>
<input :checked="checked" type="checkbox" @change="handleChange" />
<slot></slot>
</label>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
model: {
prop: 'checked',
event: 'change',
},
props: {
checked: {
type: Boolean,
default: false,
},
},
emits: ['change'],
setup(_, ctx) {
const handleChange = (e: Event) => {
const target = e.target as HTMLInputElement
ctx.emit('change', target.checked)
}
return {
handleChange,
}
},
})
</script>
# Radio
radioではlabel
propで選択肢の値を渡して、value
で選択した値を渡すようにしている。checkboxも同様の仕組みにすればBoolean以外も扱える。
<template>
<label>
<input
:value="label"
:checked="label === value"
type="radio"
@change="handleChange"
/>
<slot></slot>
</label>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
model: {
event: 'change',
},
props: {
value: {
type: [Boolean, String, Number],
default: '',
},
label: {
type: [Boolean, String, Number],
default: '',
},
},
emits: ['change'],
setup(_, ctx) {
const handleChange = (e: Event) => {
const target = e.target as HTMLInputElement
ctx.emit('change', target.value)
}
return {
handleChange,
}
},
})
</script>
# Select
selectタグはprops名value
で値を渡し、change
イベントで親コンポーネントへ変更を知らせている。
event.target
はHTMLSelectElement
として扱うことで、選択したオプションselectedOptions
を取得できる。
<template>
<select @change="handleChange">
<option
v-for="(option, index) in options"
:key="index"
:value="option.value"
:selected="value === option.value"
>
{{ option.label }}
</option>
</select>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@nuxtjs/composition-api'
interface LavelValue {
lavel: string | number
value: string | number
}
export default defineComponent({
model: {
event: 'change',
},
props: {
value: {
type: [String, Number],
default: '',
},
options: {
type: Array as PropType<LavelValue[]>,
default: () => [],
},
},
emits: ['change'],
setup(_, ctx) {
const handleChange = (e: Event) => {
const target = e.target as HTMLSelectElement
ctx.emit('change', target.selectedOptions[0].value)
}
return {
handleChange,
}
},
})
</script>
# 参考
https://jp.vuejs.org/v2/guide/forms.html (opens new window)
https://element-plus.org/ (opens new window)
https://qiita.com/wakame_isono_/items/611e51ff965d698bbc7c (opens new window)
# 関連記事
Nuxt Composition APIでv2との書き方の違いを確認した (opens new window)
Vue.jsのinputイベントがselectタグで発火しない(Internet Explorer 11)原因と対応 (opens new window)