LoginSignup
137
154

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-10

はじめに

ゆるい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>

完成品

advent calendar.png

まとめ

Vue.jsでForm部品をComponent化してみました。
Elementを参考にしています。

137
154
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
137
154