Edited at
Vue.js #3Day 10

Vue.jsでForm部品をComponent化する

More than 1 year has passed since last update.


はじめに

ゆるいAtomicDesignを意識してForm部品をComponent化してみました。


Input


input.vue

<template>

<label>
<span v-if="$slots.label"><slot name="label"></slot></span>
<span v-else-if="label">{{ label }}</span>

<input :type="type"
:name="name"
:placeholder="placeholder"
:value="value"
@input="updateValue"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
</label>
</template>
<script>
export default {
props: {
value: {},
label: { type: String },
name: { type: String, require: true },
type: { type: String, default: 'text' },
placeholder: { type: String }
},

methods: {
updateValue (e) {
this.$emit('input', e.target.value)
this.$emit('change', e.target.value)
}
}
}
</script>


グローバル登録

import Input from '@/components/input'

Vue.component('vue-input', Input)

呼び出し

<vue-input label="Input" name="input" v-model="form.input"></vue-input>

propされた値はComponent内で直接更新できないのでv-modelの糖衣構文(v-bind:value、v-on:input)を利用します。

v-bind:value

<input ...省略

:value="value"
@input="updateValue"
...省略
>

v-on:input

updateValue (e) {

this.$emit('input', e.target.value)
this.$emit('change', e.target.value) // Changeイベント
}

値が変更された場合に処理を行うためにChangeイベントもトリガさせています。

<vue-input label="Input" name="input"

v-model="form.input"
@change="handleChange" <!-- 値が変更された場合の処理 -->
></vue-input>

label部分は文字列以外の場合もあるので、slotまたはpropでコンテンツを渡します。

slot

<vue-input name="input" v-model="form.input">

  <template slot="label"> <!-- slotでlabelを渡す -->
    <span>*</span>Label
  </template>
</vue-input>

prop

<vue-input label="Input" name="input" v-model="form.input"></vue-input>


Radio


radio.vue

<template>

<label>
<span v-if="$slots.label"><slot name="label"></slot></span>
<span v-else-if="label">{{ label }}</span>

<input type="radio"
:name="name"
:value="value"
:checked="checked === value"
@change="updateValue"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
<slot></slot>
</label>
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'input'
},

props: {
value: {},
checked: {},
label: { type: String },
name: { type: String, require: true }
},

methods: {
updateValue (e) {
this.$emit('input', this.value)
this.$emit('checked', this.value)
}
}
}
</script>


グローバル登録

import Radio from '@/components/radio'

Vue.component('vue-radio', Radio)

呼び出し

<vue-radio label="Radio" name="radio1" :value="1" v-model="form.radio1">1</vue-radio>

Radioにはvalue属性があり、v-bind:valueに競合するため、modelを使ってv-bind:valueをv-bind:checkedに変更します。

model: {

prop: 'checked', // v-bind:checked
event: 'input' // v-on:input
}


Radio Group

複数のRadioを扱いたい場合があるかもしれません。

その場合はグループ用のComponentを利用します。


group.vue

<template>

<div class="vue-group">
<label v-if="$slots.label"><slot name="label"></slot></label>
<label v-else-if="label">{{ label }}</label>

<slot></slot>
</div>
</template>
<script>
export default {
props: {
value: {},
label: { type: String }
}
}
</script>

Radio Componentを変更します。

<template>

<label>
<span v-if="$slots.label"><slot name="label"></slot></span>
<span v-else-if="label">{{ label }}</span>

<input type="radio"
:name="name"
:value="value"
:checked="input === value"
@change="updateValue"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
<slot></slot>
</label>
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'input'
},

props: {
value: {},
checked: {},
label: { type: String },
name: { type: String, require: true }
},

computed: {
group () {
return ('$parent' in this && this.$parent.$options.name === 'vue-group') ? this.$parent : null
},
input () {
return this.group ? this.group.value : this.checked
}
},

methods: {
updateValue (e) {
if (this.group) {
this.group.$emit('input', this.value)
this.group.$emit('checked', this.value)
} else {
this.$emit('input', this.value)
this.$emit('checked', this.value)
}
}
}
}
</script>

グローバル登録

import Radio from '@/components/radio'

import Radio from '@/components/radio'
Vue.component('vue-radio', Radio)
Vue.component('vue-group', Group)

呼び出し

<vue-group label="Radio" v-model="form.radio2">

<vue-radio name="radio2" :value="1">1</vue-radio>
<vue-radio name="radio2" :value="2">2</vue-radio>
</vue-group>

this.$parentが存在し、それがGroup ComponentならばGroup Componentのv-modelを利用します。


CheckBox

CheckBoxはRaidoとほぼ同様です。


check.vue

<template>

<label>
<span v-if="$slots.label"><slot name="label"></slot></span>
<span v-else-if="label">{{ label }}</span>

<input type="checkbox"
:name="name"
:value="value"
:checked="input === value"
@change="updateValue"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
<slot></slot>
</label>
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'input'
},

props: {
value: {},
checked: {},
label: { type: String },
name: { type: String, require: true }
},

computed: {
group () {
return ('$parent' in this && this.$parent.$options.name === 'vue-group') ? this.$parent : null
},
input () {
return this.group ? this.group.value : this.checked
}
},

methods: {
updateValue (e) {
let value = e.target.checked ? this.value : null
if (this.group) {
this.group.$emit('input', value)
this.group.$emit('checked', value)
} else {
this.$emit('input', value)
this.$emit('checked', value)
}
}
}
}
</script>


グローバル登録

import Check from '@/components/check'

Vue.component('vue-check', Check)

呼び出し

<vue-check label="Check" name="check" :value="1" v-model="form.check">1</vue-check>


Select


select.vue

<template>

<label>
<span v-if="$slots.label"><slot name="label"></slot></span>
<span v-else-if="label">{{ label }}</span>

<select :name="name"
:value="value"
@input="updateValue"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
<slot :options="options"></slot>
</select>
</label>
</template>
<script>
export default {
props: {
value: {},
label: { type: String },
name: { type: String, require: true },
options: { type: Array, required: true }
},

methods: {
updateValue (e) {
let value = e.target.selectedOptions.length ? e.target.selectedOptions[0]._value : null
this.$emit('input', value)
this.$emit('selected', value)
}
}
}
</script>


呼び出し

<vue-select label="Select" name="select" v-model="form.select" :options="options">

<template slot-scope="props">
<option v-for="(option, key) in props.options"
:key="key"
:value="option.id"
:selected="form.select === option.id"
>{{ option.value }}</option>
</template>
</vue-select>

非同期でoptionsを読み込み糖衣構文を利用すると、v−modelで指定したoptionが選択状態にならないので、selectedを利用して選択状態にします。

<option ...省略

:selected="form.select === option.id"
>{{ option.value }}</option>

選択したoptionのvalueはString型になるので元々の型がbindされている_valueを利用します。

e.target.selectedOptions[0]._value // e.target.selectedOptions[0].valueはString型

最後に、main.jsとapp.vueです。


main.js


main.js

// The Vue build version to load with the `import` command

// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from '@/app'

import Input from '@/components/input'
import Check from '@/components/check'
import Radio from '@/components/radio'
import Select from '@/components/select'
import Group from '@/components/group'
Vue.component('vue-input', Input)
Vue.component('vue-check', Check)
Vue.component('vue-radio', Radio)
Vue.component('vue-select', Select)
Vue.component('vue-group', Group)

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
el: '#app',
template: '<App/>',
components: { App }
})



app.vue


app.vue

<template>

<div id="app">
<form @submit.prevent="onSubmit">
<div class="form-item">
<vue-input label="Input" name="input" v-model="form.input"></vue-input>
- {{ form.input }}
</div>
<div class="form-item">
<vue-radio name="radio1" :value="1" v-model="form.radio1">1</vue-radio>
<vue-radio name="radio1" :value="2" v-model="form.radio1">2</vue-radio>
- {{ form.radio1 }}
</div>
<div class="form-item">
<vue-group label="Radio" v-model="form.radio2">
<vue-radio name="radio2" :value="1">1</vue-radio>
<vue-radio name="radio2" :value="2">2</vue-radio>
</vue-group>
- {{ form.radio2 }}
</div>
<div class="form-item">
<vue-check label="Check" name="check" :value="1" v-model="form.check">1</vue-check>
- {{ form.check }}
</div>
<div class="form-item">
<vue-select label="Select" name="select" v-model="form.select" :options="options">
<template slot-scope="props">
<option v-for="(option, key) in props.options"
:key="key"
:value="option.id"
:selected="form.select === option.id"
>{{ option.value }}</option>
</template>
</vue-select>
- {{ form.select }}
</div>
<div class="form-item">
<button type="submit">送信</button>
</div>
</form>
</div>
</template>

<script>
export default {
name: 'app',

data: () => ({
form: {
input: 'test',
radio1: 1,
radio2: 1,
check: 1,
select: 3
},
options: [
{ id: 1, value: 'option1' },
{ id: 2, value: 'option2' },
{ id: 3, value: 'option3' },
{ id: 4, value: 'option4' }
]
}),

methods: {
onSubmit () {
console.log('送信')
}
}
}
</script>



完成品


まとめ

Vue.jsでForm部品をComponent化してみました。

Elementを参考にしています。