Vue.jsでFormの各要素をComponent化する際の覚え書き

2020/04/10 追記
Vue-Composition-API版を書きました。
↓の記事のv-bind.syncを使うほうが実用的です。
[Vue\.jsでForm要素のラッパーコンポーネントを作る際の2つの方法（v\-bind\.sync, v\-model） \- Qiita](https://qiita.com/ryo2132/items/44bd05f2b1b66df26597)

---

Vue.jsでFormの各フィールドをcomponent化する際の実装例です。
コンポーネント化することで、デザイン、バリデーションなどが全Formで共通化できます。
流行りのAtomicデザインを適応する際などに良いかと思います。

以下コードは、全てこちらのリポジトリにあります。
https://github.com/kawamataryo/Vue-atomic-form

# Input
まず基本のinputのcomponet実装例。
フィールドの値は、`@input`で変更を監視して、`$emit`の`'input'`を利用し親コンポーネントのv-modelに値を送っています。
`updateValue`が引数で受け取る`e`には`event`が入っているので、そこから`taget.value`で値を取得して`emit`で親コンポーネントに渡しています。

```vue:MyInput.vue
<template>
  <input
    :type="type"
    :name="name"
    :value="value"
    :placeholder="placeholder"
    @input="updateValue"
  />
</template>

<script>
export default {
  name: "MyInput",
  props: {
    value: { type: String, required: true },
    type: { type: String, required: true },
    name: { type: String, required: true },
    placeholder: { type: String, required: false }
  },
  methods: {
    updateValue: function(e) {
      this.$emit("input", e.target.value);
    }
  }
};
</script>
```

呼び出し元は、、

```vue:App.vue
<MyInput
  v-model="sampleForm.text"
  placeholder="サンプル"
  name="sample-input"
  type="text"
></MyInput>
```

# Textarea
基本的にinputと同様の実装です。
typeを外してrowsとcolsを追加しました。

```vue:MyTextarea.vue
<template>
  <textarea
    :name="name"
    :value="value"
    :placeholder="placeholder"
    :rows="rows"
    :cols="cols"
    @input="updateValue"
  ></textarea>
</template>

<script>
export default {
  name: "MyTextarea",
  props: {
    value: { type: String, required: true },
    name: { type: String, required: true },
    placeholder: { type: String, required: false },
    rows: { type: Number, required: false },
    cols: { type: Number, required: false }
  },
  methods: {
    updateValue: function(e) {
      this.$emit("input", e.target.value);
    }
  }
};
</script>
```

呼び出し元は

```vue:App.vue
<MyTextarea
  v-model="sampleForm.textarea"
  placeholder="サンプル"
  name="sample-textarea"
  :rows="10"
  :cols="50"
></MyTextarea>
```

# Radio Button
radioボタンの場合は、要素を`options`として配列で渡すようにしています。
あとはコンポーネント内で`v-for`で`options`を回し、内部の`value`と`label`を表示します。
`updateValue`は同様です。

```vue:MyRadio
<template>
  <fieldset>
    <template v-for="(option, index) in options">
      <label :key="index">
        <input
          type="radio"
          :name="name"
          :value="option.value"
          @change="updateValue"
        />{{ option.label }}
      </label>
    </template>
  </fieldset>
</template>

<script>
export default {
  name: "MyRadio",
  props: {
    value: { type: String, required: true },
    options: { type: Array, required: true },
    name: { type: String, required: true },
  },
  methods: {
    updateValue: function(e) {
      this.$emit("input", e.target.value);
    }
  }
};
</script>
```

呼び出し元はoptionsに表示したいラベルと値を持つオブジェクトの配列を`options`として渡します。

```vue:App.vue
// 変数optionsには[{label: "hoge", value: "1"}, {label: "fuga", value: "2"}]のようなオブジェクトの配列を設定
<MyRadio
  v-model="sampleForm.radio"
  name="sample-radio"
  :options="options"
></MyRadio>
```

# Select
基本はRadioボタンと同様に`options`で値とラベルのオブジェクトの配列を渡し`v-for`で表示します。
ただ、selectの場合、初期で選択されているので、何も変更しない場合`@change`での`updateValue`が呼ばれず、値が空になってしまいます。
それを防ぐため、`mouted()`で初期選択時の値を`$emit`で送っています。

```vue:select.vue
<template>
  <fieldset>
    <select :name="name" @change="updateValue">
      <template v-for="(option, index) in options">
        <option :value="option.value" :key="index">
          {{ option.label }}
        </option>
      </template>
    </select>
  </fieldset>
</template>

<script>
export default {
  name: "MySelect",
  props: {
    value: { type: String, required: true },
    options: { type: Array, required: true },
    name: { type: String, required: true },
  },
  methods: {
    updateValue: function(e) {
      this.$emit("input", e.target.value);
    }
  },
  mounted() {
    this.$emit("input", this.options[0].value);
  }
};
</script>
```

呼び出し元はradioボタンの時と同様に`options`で表示したい値、ラベルを渡します。

```vue:App.vue
```App.vue
// 変数optionsには[{label: "hoge", value: "1"}, {label: "fuga", value: "2"}]のようなオブジェクトの配列を設定
<MySelect
  v-model="sampleForm.select"
  name="sample-select"
  :options="options"
></MySelect>
```


# Checkbox
これが一番苦労しました。
一見ラジオボタンと同様なのですが、複数選択可能なので、配列で値を返す必要があります。
`data`で`values`を持ち`updateValue`では、チェック状態を判定してその`values`に値を追加、削除を行っています。
そして`emit`で返すのは`data`として持っている`values`です。

```vue:MyCheckbox.vue
<template>
  <fieldset>
    <template v-for="(option, index) in options">
      <label :key="index">
        <input
          type="checkbox"
          :name="name"
          :value="option.value"
          @change="updateValue"
        />{{ option.label }}
      </label>
    </template>
  </fieldset>
</template>

<script>
export default {
  name: "MyCheckbox",
  props: {
    options: { type: Array, required: true },
    name: { type: String, required: true },
  },
  data() {
    return {
      values: []
    };
  },
  methods: {
    updateValue: function(e) {
      if (e.target.checked) {
        this.values.push(e.target.value);
      } else {
        this.values = this.values.filter(v => v !== e.target.value);
      }
      this.$emit("input", this.values);
    }
  }
};
</script>
```

呼び出し元はこちらです。

```App.vue
// 変数optionsには[{label: "hoge", value: "1"}, {label: "fuga", value: "2"}]のようなオブジェクトの配列を設定
<MyCheckbox
  v-model="sampleForm.checkbox"
  name="sample-checkbox"
  :options="options"
></MyCheckbox>
```

# Form全体
結果としてこのようなcomponentが集約されたフォームができます。

```vue:App.vue
<template>
  <div id="app">
    <form action="">
      <MyLabel>InputText</MyLabel>
      <MyInput
        v-model="sampleForm.text"
        placeholder="サンプル"
        name="sample-input"
        type="text"
      ></MyInput>
      <MyLabel>TextArea</MyLabel>
      <MyTextarea
        v-model="sampleForm.textarea"
        placeholder="サンプル"
        name="sample-textarea"
        :rows="10"
        :cols="50"
      ></MyTextarea>
      <MyLabel>RadioButton</MyLabel>
      <MyRadio
        v-model="sampleForm.radio"
        name="sample-radio"
        :options="options"
      ></MyRadio>
      <MyLabel>Checkbox</MyLabel>
      <MyCheckbox
        v-model="sampleForm.checkbox"
        name="sample-checkbox"
        :options="options"
      ></MyCheckbox>
      <MyLabel>Select</MyLabel>
      <MySelect
        v-model="sampleForm.select"
        name="sample-select"
        :options="options"
      ></MySelect>
      <MyBtn @click="sendForm">send</MyBtn>
    </form>
  </div>
</template>

<script>
import MyInput from "./components/MyInput";
import MyTextarea from "./components/MyTextarea";
import MyRadio from "./components/MyRadio";
import MyCheckbox from "./components/MyCheckbox";
import MySelect from "./components/MySelect";
import MyBtn from "./components/MyBtn";
import MyLabel from "./components/MyLabel";

export default {
  name: "app",
  components: {
    MyTextarea,
    MyInput,
    MyRadio,
    MyCheckbox,
    MySelect,
    MyBtn,
    MyLabel
  },
  data() {
    return {
      sampleForm: {
        text: "",
        radio: "",
        select: "",
        textarea: "",
        checkbox: []
      },
      options: [
        {
          label: "fizz",
          value: "3"
        },
        {
          label: "buzz",
          value: "5"
        },
        {
          label: "fizzBuzz",
          value: "15"
        }
      ]
    };
  },
  methods: {
    sendForm() {
      // formデータの送信処理
    }
  }
};
</script>
```

# 参考
Vue.jsでForm部品をComponent化する
https://qiita.com/wakame_isono_/items/611e51ff965d698bbc7c