Vuejs 追加と編集フォームを共通化する

課題

追加と編集フォームは同じデータを操作することが多い。そのため、各フォームを異なるコンポーネントとして作成するとほぼ同じ処理をするコンポーネントが2つできることになり、入力チェックやレイアウトの調整に2重のコストがかかる。そのため、追加と編集フォームを共通化したい。

解決方法

あまりスマートな方法を見つけられなかったので、以下、愚直に実装した例。

  • 子コンポーネントに、入力チェックや画面のレイアウトなどの共通処理を実装する
  • 子コンポーネントで入力チェックが終わったデータを、親コンポーネントにイベントとして通知する
  • 親コンポーネントで子からイベントを受け取り、追加/編集固有の処理ロジックを実装する

f:id:kimulla:20191201223558p:plain

検証環境

package.json

...
  "dependencies": {
    "vue": "^2.5.2",
    "vue-router": "^3.0.1"
  },
...

子コンポーネント

  • 初期表示するためのデータを親からpropsで受け取る
  • 子コンポーネントに、入力チェックや画面のレイアウトなどの共通処理を実装する
  • 入力チェック等が終わったデータを、親コンポーネントにイベントとして通知する
<template>
  <div class="panel panel-default">
    <div class="panel-heading">
      {{title}}
    </div>
    <div class="panel-body">
      <form @submit.prevent="submit">
        <div class="form-group">
          <label for="name">名前</label>
          <input type="text" class="form-control" id="name" v-model="form.name" required maxlength="5"/>
        </div>
        <div class="form-group">
          <label for="role">権限</label>
          <select v-model="form.selectedRoleId" id="role" class="form-control" required>
            <option v-for="role in roles" :value="role.id" :key="role.id">
              {{role.name}}
            </option>
          </select>
        </div>
        <button type="submit">保存する</button>
      </form>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Form',
  props: {
    initialName: String,
    initialSelectedRoleId: Number,
    title: String
  },
  data () {
    return {
      roles: [],
      form: {
        name: this.initialName,
        selectedRoleId: this.initialSelectedRoleId
      }
    }
  },
  mounted: function () {
    // serverからデータを取得する
    this.roles = [
      {
        id: 1,
        name: 'admin'
      },
      {
        id: 2,
        name: 'leader'
      },
      {
        id: 3,
        name: 'member'
      }
    ]
  },
  methods: {
    submit: function () {
      // validationやらの入力チェックをして、okだったらsubmitイベントを出す
      console.log('validation ... ')
      this.$emit('submit', this.form)
    }
  }
}
</script>

親コンポーネント

  • 子のsubmitイベントを拾ってサーバへの追加といった固有処理を記述する
<template>
  <base-form
    title="ユーザの追加"
    @submit="add"
  ></base-form>
</template>

<script>
import BaseForm from './BaseForm'

export default {
  name: 'AddForm',
  methods: {
    add: function (form) {
      // サーバに保存する、など
      console.log('send server ...', JSON.stringify(form))
    }
  },
  components: { BaseForm }
}
</script>
  • 編集フォームも同じ
<template>
  <base-form
    title="ユーザの編集"
    :initialName="name"
    :initialSelectedRoleId="selectedRoleId"
    @submit="edit"
  ></base-form>
</template>

<script>
import BaseForm from './BaseForm'

export default {
  name: 'EditForm',
  data () {
    return {
      name: '',
      selectedRoleId: ''
    }
  },
  created: function () {
    // サーバから最新データを取得する
    this.name = '管理し太郎'
    this.selectedRoleId = 1
  },
  methods: {
    edit: function (form) {
      // サーバに保存する、など
      console.log('send server ...', JSON.stringify(form))
    }
  },
  components: { BaseForm }
}
</script>

実行結果

  • 追加と編集フォームを共通化できた

f:id:kimulla:20191201223627g:plain

補足

今回は親子間でデータの同期をしていないが、v-modelでデータを同期したければ以下が参考になる。
参考 github issues

親コンポーネント側でコンテンツを差し替えたい要素があれば<slot>を利用する。

今回は$emitのやりとりで親コンポーネントにデータを渡しているが、子側に追加/編集フォーム固有処理を記述したイベントハンドラを渡す方法もある。処理は異なるけど見た目とロジックの一部は再利用したい、という場合のベストプラクティスがよくわかってないので、これ良いよという方法があれば教えてほしいです!