Vue Component 技巧 - 共用 Form
本文會大量使用 Component 一詞,但是它會有兩個意思,在此先進行名詞定義:
- Vue component,指的是 Vue 框架的一種寫法。
- Component,指的是 pure component,一種輸出永遠只和輸入有關,Component 本身並沒有存任何狀態(資料),如果設計上需要,要能跨 Page 共用。
在 Vue 中,常見的做法就是將 Component 分層次
在此我們以簡單的層次 Page 和 Component 為例,介紹一下如何做好「表單」這樣的 Component
看此文需具備以下使用經驗
- Webpack
- vue-cli
- vue component
Page 和 Component
在開始之前,先解釋一下這個層次的分法與原則,雖然很粗略,卻是在學好一個前端框架之後,必須具備的進階觀念
- Page: 每一頁都是一個 Vue component,負責某些特定資料的載入
- Component: 每一個 Component 都代表一個處理資料的方式,有些是顯示方式,有些是維護方式,各有不同,也許不會直接處理資料的全部欄位。
先說情境,再說說要合併什麼 Component
使用 Element 當 UI Component
這兩個 Vue Component、有三個欄位
- 角色:
<select ....
- 帳號:
<input type="text"
- 密碼: 一般的文字顯示
可以看一下這個 vue 大概的樣貌
帳號編輯的頁面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <template> <h1>帳號設定</h1> <h2>基本資料</h2> <el-form label-position="top" :model="user"> <el-row :gutter="20"> <el-col> <el-form-item label="角色"> <el-select disabled v-model="roleName" placeholder="請選擇"> <el-option v-for="item in rolesOptions" :key="item.name" :label="item.name" :value="item.name"> </el-option> </el-select> </el-form-item> </el-col> <el-col :xs="24" :sm="12"> <el-form-item label="登入帳號"> <el-input v-model="account" disabled> </el-input> </el-form-item> </el-col> <el-col :xs="24" :sm="12"> <el-form-item label="密碼"> <div>{{ password }}</div> </el-form-item> </el-col> </el-row> <el-form-item> <el-button type="primary" @click="onSubmit"> 確定送出 </el-button> </el-form-item> </el-form> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import ToPathMixin from '@/mixins/ToPath' export default { mixins: [ToPathMixin], computed: { user() { return this.$store.getters.currentUser }, name: { get() { return this.$store.getters.currentUser.name }, set(value) { this.$store.commit('currentUserName', value) } }, roleName: { get() { return this.$store.getters.currentUser.roleName }, set(value) { this.$store.commit('currentUserRoleName', value) } }, rolesOptions() { return this.$store.getters.roles }, account: { get() { return this.$store.getters.currentUser.account }, set(value) { this.$store.commit('currentUserAccount', value) } } }, methods: { async onSubmit() { try { await this.$store.dispatch('updateUser', { userId: this.$route.params.userId }) this.$message({ message: `成功編輯 ${this.user.name}`, type: 'success', center: true, duration: 1800 }) this.toPath('Users') } catch (e) { this.$message.error(`請重新檢查 ${e.message}`) } } } }
|
新增帳號的頁面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <template> <h1>帳號設定</h1> <h2>基本資料</h2> <el-form label-position="top" :model="user"> <el-row :gutter="20"> <el-col> <el-form-item label="角色"> <el-select v-model="roleName" placeholder="請選擇"> <el-option v-for="item in rolesOptions" :key="item.name" :label="item.name" :value="item.name"> </el-option> </el-select> </el-form-item> </el-col> <el-col :xs="24" :sm="12"> <el-form-item label="登入帳號"> <el-input v-model="account"> </el-input> </el-form-item> </el-col> <el-col :xs="24" :sm="12"> <el-form-item label="密碼"> <div>{{ password }}</div> </el-form-item> </el-col> </el-row> <el-form-item> <el-button type="primary" @click="onSubmit"> 確定送出 </el-button> </el-form-item> </el-form> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import ToPathMixin from '@/mixins/ToPath' export default { mixins: [ToPathMixin], computed: { user() { return this.$store.getters.currentUser }, name: { get() { return this.$store.getters.currentUser.name }, set(value) { this.$store.commit('currentUserName', value) } }, roleName: { get() { return this.$store.getters.currentUser.roleName }, set(value) { this.$store.commit('currentUserRoleName', value) } }, rolesOptions() { return this.$store.getters.roles }, account: { get() { return this.$store.getters.currentUser.account }, set(value) { this.$store.commit('currentUserAccount', value) } } }, methods: { async onSubmit() { try { await this.$store.dispatch('createUser', { userId: this.$route.params.userId }) this.$message({ message: `成功編輯 ${this.user.name}`, type: 'success', center: true, duration: 1800 }) this.toPath('Users') } catch (e) { this.$message.error(`請重新檢查 ${e.message}`) } } } }
|
template 的差異
第 8 行和第 20 行有 disabled
的差異。
script 的差異
只有 39 行,要呼叫的 action
有差而已。
重新定義問題
這是一個表單,新增與編輯都會用到相同的欄位
差異只有編輯時,有些欄位必須唯讀不可改,送出時所呼叫的程式也不同。
Form 很常見,用來
- 顯示特定 Object
(太長的表單可能顯示多個 Object 也許就該拆成各別 Object 各別表單,只是剛好顯示在同一個畫面上)
- 編輯欄位,將資料修改成輸入的樣子
- 唯讀欄位,不修改資料
在這個例子,要處理的大概就是這樣,而目前的兩份程式碼,複雜又重複,而且也無法直接看出上面三點的目的,要顯示什麼特定的 Object ?要編輯什麼欄位?哪些唯讀?
另外,這次要做到的,並不是增加一個 isEdit
的 props 來決定狀態,改變是否欄位唯讀,這個做法只是在增加複雜度而已,最好的做法就是可以將「不修改資料」和「唯讀」綁在一起。
開始合併
先建立一個新的 Vue component 在此叫做 MemberForm.vue
先預先把這個 component 放到原本的 Page 上面
example: 新增帳號的頁面
h1
為頁面標題,所以不移動到 form
裡面
:member
丟進主要的 Object
<template> <h1>帳號設定</h1> <member-form :member="$store.getters.currentUser" /> </template>
|
剩下的我們最後再回來處理
新增帳號頁面
將新增帳號的 template
搬進來
目前唯一的 prop: member
- 將
v-model
拆成 :value
和 @input
:value
應該 binding 到 Object property
@input
用 $emit
將值拋出去
- 唯讀顯示用 Object property
<template> <h1>帳號設定</h1> <h2>基本資料</h2> <el-form label-position="top" :model="user"> <el-row :gutter="20"> <el-col> <el-form-item label="角色"> <el-select :value="member.roleName" @input="$emit('onChangeRoleName', $event)" placeholder="請選擇"> <el-option v-for="item in rolesOptions" :key="item.name" :label="item.name" :value="item.name"> </el-option> </el-select> </el-form-item> </el-col> <el-col :xs="24" :sm="12"> <el-form-item label="登入帳號"> <el-input :value="member.account" @input="$emit('onChangeAccount', $event)"> </el-input> </el-form-item> </el-col> <el-col :xs="24" :sm="12"> <el-form-item label="密碼"> <div>{{ member.password }}</div> </el-form-item> </el-col> </el-row> <el-form-item> <el-button type="primary" @click="$emit('onSubmit', $event)"> 確定送出 </el-button> </el-form-item> </el-form> </template>
|
新增帳號的 script 也搬進來
原本 v-model
的 getter 拿掉,而 setter 也變成 event 也可以拿掉了。
- 先設定 prop
- 將主要的 Object 放進來
- 下拉式選單的選項也要外部給
1 2 3 4 5 6
| export default { props: { member: Object, rolesOptions: Array, } }
|
回去處理上一層 component
將原本 v-model
binding vuex 的 computed 拆開。
- getter 用不到了
- setter 的內容放到自定義的 event 裡,將
$event
當參數
<template> <h1>帳號設定</h1> <member-form :member="$store.getters.currentUser" @onChangeRoleName="$store.commit('currentUserRoleName', $event)" @onChangeAccount="$store.commit('currentUserAccount', $event)" @onSubmit="onSubmit" /> </template>
|
1 2 3 4 5 6 7 8 9 10 11
| import ToPathMixin from '@/mixins/ToPath' import MemberForm from '@/components/MemberForm' export default { mixins: [ToPathMixin], components: { MemberForm }, methods: { onSubmit() { } } }
|
這樣應該就可以正常運作原本的頁面。
處理編輯帳號的頁面
直接將 template
<template> <div class="userEdit"> <h1>帳號設定</h1> <member-form :member="$store.getters.currentUser" :rolesOptions="this.$store.getters.roles" @onChangeRoleName="$store.commit('currentUserRoleName', $event)" @onSubmit="onSubmit" /> </div> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12
| import ToPathMixin from '@/mixins/ToPath' import MemberForm from '@/components/MemberForm' export default { name: 'UserEdit', mixins: [ToPathMixin], components: { MemberForm }, methods: { submit() { } } }
|
自動唯讀切換 - 唯讀用 disabled
再回到 Member.vue 加上「編輯帳號」需要的唯讀欄位
原本的帳號編輯是使用 disabled
就先以此為實作目標
怎麼加才好呢?
vue 提供 vm.$listeners
讓開發 component 的人可以知道,外部有宣告什麼自訂義的 event ?
在此,將它使用在每一個可以輸入的地方,如果外部呼叫沒有要改變值,就將它 disabled
<el-form-item label="角色"> <el-select :value="member.roleName" @input="$emit('onChangeRoleName', $event)" :disabled="!$listeners.onChangeRoleName" placeholder="請選擇"> <el-option v-for="item in rolesOptions" :key="item.name" :label="item.name" :value="item.name"> </el-option> </el-select> </el-form-item>
|
<el-input :value="member.account" @input="$emit('onChangeAccount', $event)" :disabled="!$listeners.onChangeAccount" > </el-input>
|
這樣一來,在兩頁的 <member-from />
直接比較之下,就可以知道哪些欄位有被 disable 了。
新增帳號
會改變 RoleName 和 Account
<template> <h1>帳號設定</h1> <member-form :member="$store.getters.currentUser" @onChangeRoleName="$store.commit('currentUserRoleName', $event)" @onChangeAccount="$store.commit('currentUserAccount', $event)" @onSubmit="onSubmit" /> </template>
|
編輯帳號
只能修改會改變 RoleName
Account 唯讀,所以自動成為 disabled
<template> <div class="userEdit"> <h1>帳號設定</h1> <member-form :member="$store.getters.currentUser" :rolesOptions="this.$store.getters.roles" @onChangeRoleName="$store.commit('currentUserRoleName', $event)" @onSubmit="onSubmit" /> </div> </template>
|
自動唯讀切換 - 唯讀用 text
在 MemberForm.vue 唯讀欄位
利用 vm.$listeners
判斷是否要修改,這次改成用 v-if
可以切換顯示的形式,改成一般的字串。
<el-form-item label="角色"> <el-select v-if="$listeners.onChangeRoleName" :value="member.roleName" @input="$emit('onChangeRoleName', $event)" placeholder="請選擇"> <el-option v-for="item in rolesOptions" :key="item.name" :label="item.name" :value="item.name"> </el-option> </el-select> <span v-if="!$listeners.onChangeRoleName">{{member.roleName}}</span> </el-form-item>
|
<el-input v-if="$listeners.onChangeAccount" :value="member.account" @input="$emit('onChangeAccount', $event)" > </el-input> <span v-if="$listeners.onChangeAccount">{{member.account}}</span>
|
下拉式選單的選項在唯讀時,也可以不用放進 MemberForm 裡了