課題
追加と編集フォームは同じデータを操作することが多い。そのため、各フォームを異なるコンポーネントとして作成するとほぼ同じ処理をするコンポーネントが2つできることになり、入力チェックやレイアウトの調整に2重のコストがかかる。そのため、追加と編集フォームを共通化したい。
解決方法
あまりスマートな方法を見つけられなかったので、以下、愚直に実装した例。
- 子コンポーネントに、入力チェックや画面のレイアウトなどの共通処理を実装する
- 子コンポーネントで入力チェックが終わったデータを、親コンポーネントにイベントとして通知する
- 親コンポーネントで子からイベントを受け取り、追加/編集固有の処理ロジックを実装する
検証環境
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>
実行結果
- 追加と編集フォームを共通化できた
補足
今回は親子間でデータの同期をしていないが、v-model
でデータを同期したければ以下が参考になる。
参考 github issues
親コンポーネント側でコンテンツを差し替えたい要素があれば<slot>
を利用する。
今回は$emit
のやりとりで親コンポーネントにデータを渡しているが、子側に追加/編集フォーム固有処理を記述したイベントハンドラを渡す方法もある。処理は異なるけど見た目とロジックの一部は再利用したい、という場合のベストプラクティスがよくわかってないので、これ良いよという方法があれば教えてほしいです!