LoginSignup
288
266

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-02-02

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


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で親コンポーネントに渡しています。

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>

呼び出し元は、、

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

Textarea

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

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>

呼び出し元は

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

Radio Button

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

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として渡します。

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で送っています。

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で表示したい値、ラベルを渡します。

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

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

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が集約されたフォームができます。

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

288
266
2

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
288
266