本案例將會講解如何使用 vue.js + ElementUI 開發一個簡單的 可配置組合表單 Demo。
示例原始碼 github
操作演示(GIF 較大):
在左側新建表單區塊,選擇區塊標題和表單元件型別後點選確定,會在中間區域生成一個塊新的表單,右側展示了所有表單的資料合併結果。
在本示例中你主要可以看到以下知識點的運用:
- vue.js 單檔案元件,
- 元件傳參
- 自定義 v-model
- 資料監聽
- 資料合併
- 批量自動註冊元件
- 使用 mixin 抽取公用程式碼
- sass 語法
- BEM 規範
- 儘量避免使用 for 迴圈的寫法
<component>
元件- 動態繫結 v-model 到一組資料
上面列舉的這些是因為以前有群裡朋友詢問相關的實現方法,在此列出,可能正在讀這篇文章的你已經都掌握了,恭喜你!(本篇文章的起因也是群友提問)
下面開始正文
總覽
這個 demo 的所有元件和邏輯如果寫在一個檔案中大概會有幾百行,維護起來會有麻煩,所以首先設計這樣的目錄結構:
搭建基本框架
為了快速開發頁面本專案使用 ElementUI 和 D2Admin 快速搭建,以下示例中元件都來自這兩個開源專案,如果你不認識這些元件也沒有關係,大致瞭解意思就可。
首先寫出頁面的大致框架:
<template>
<d2-container>
<template slot="header">可配置問卷示例</template>
<div class="questionnaire">
<el-container>
<!-- 左側位置 -->
<!-- 中間位置 -->
<!-- 右側位置 -->
</el-container>
</div>
<template slot="footer">從左側選擇要新增的表單塊,右側檢視結果</template>
</d2-container>
</template>
複製程式碼
<script>
export default {
name: `page1`,
components: {
// 這裡以後要要登錄檔單區塊 左側邊欄 右側邊欄
},
data () {
return {
formList: [], // 所有註冊的表單區塊
forms: [] // 使用者已經選擇的表單區塊
}
}
}
</script>
複製程式碼
css / sass 暫時先忽略,在最後會展示樣式程式碼
表單區塊
新建 page1/components/Form/Form1.vue
作為第一個表單區塊
<template>
<el-form ref="form" :model="form" label-position="top">
<el-form-item label="姓名">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="姓名">
<el-radio-group v-model="form.usersex">
<el-radio :label="1">男</el-radio>
<el-radio :label="0">女</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: `Form1`,
props: {
value: {
default: () => ({
username: ``,
usersex: 1
})
}
},
data () {
return {
form: {
username: ``,
usersex: 1
}
}
},
watch: {
form: {
// 處理方法
handler (value) {
this.$emit(`input`, value)
},
// 深度 watch
deep: true,
// 首先自己執行一次
immediate: true
}
}
}
</script>
複製程式碼
這是用 ElementUI 構建的很簡單的一個表單,甚至沒有校驗。
然後我們在頁面元件上註冊這個表單區塊:
<script>
components: {
// 註冊元件
Form1: () => import(`./components/Form/Form1.vue`)
},
data () {
return {
// 註冊到資料
formList: [
{
title: `基礎`,
name: `Form1`
}
]
}
}
</script>
複製程式碼
等等,假如我有 20 個區塊,難道要寫 20 遍註冊,在 formList 裡手動加 20 個物件嗎?
所以我們先新建了 7 個區塊,區塊內容都大同小異,並將程式碼稍加改造:
表單區塊示例
<template>
<el-form ref="form" :model="form" label-position="top">
<el-form-item label="姓名">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="姓名">
<el-radio-group v-model="form.usersex">
<el-radio :label="1">男</el-radio>
<el-radio :label="0">女</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</template>
<script>
export default {
// 排序使用
index: 1,
// 元件標題
title: `基礎`,
// 元件名
name: `Form1`,
props: {
value: {
default: () => ({
username: ``,
usersex: 1
})
}
},
data () {
return {
form: {
username: ``,
usersex: 1
}
}
},
watch: {
form: {
handler (value) {
this.$emit(`input`, value)
},
deep: true,
immediate: true
}
}
}
</script>
複製程式碼
頁面元件(只展示重點部分)
<script>
import sortby from `lodash.sortby`
const req = context => context.keys().map(context)
const forms = req(require.context(`./components/Form/`, false, /.vue$/))
const components = {}
const formList = []
sortby(forms.map(e => {
const component = e.default
const { index, title, name } = component
return { component, title, index, name }
}), [`index`]).forEach(form => {
const { component, title, name } = form
components[name] = component
formList.push({ title, name })
})
export default {
components,
data () {
return {
formList
}
}
}
</script>
複製程式碼
你可能要問,上面這一大坨是什麼鬼 ???
首先介紹 webpack 的 require-context 你可以點選連結檢視官方文件。
簡單通俗來講這個方法就是為了方便引入大量檔案用的,它接收三個引數
- 你要引入檔案的目錄
- 是否要查詢該目錄下的子級目錄
- 匹配要引入的檔案
然後會返回一個 require 物件,物件有三個屬性:resolve 、keys、id
- resolve: 是一個函式,他返回的是被解析模組的id
- keys: 也是一個函式,他返回的是一個陣列,該陣列是由所有可能被上下文模組解析的請求物件組成
- id:上下文模組的id
所以在上面程式碼中
const req = context => context.keys().map(context)
const forms = req(require.context(`./components/Form/`, false, /.vue$/))
複製程式碼
最後得到的 forms 就是 ./components/Form/
目錄下所有的 vue 檔案物件
然後通過
sortby(forms.map(e => {
const component = e.default
const { index, title, name } = component
return { component, title, index, name }
}), [`index`]).forEach(form => {
const { component, title, name } = form
components[name] = component
formList.push({ title, name })
})
複製程式碼
處理 forms 物件,得到 vue 註冊元件時需要的的 components 格式,並且將所有的元件資訊儲存進 formList 供頁面邏輯使用。具體的轉換方式請檢視上面的程式碼。
這樣不管我們在 ./components/Form/
下寫了多少單檔案元件,webpack 都會自動幫我們引入並通過我們的程式碼註冊到頁面中。
大量元件註冊的問題解決了,接下來我們還要一個需要優化的問題:
不管是 Form1 還是 Form2 還是 FormN,大家會發現其實程式碼裡有一些重複內容,還有一些是有邏輯關係的重複內容,下面我們通過寫一個 mixin 來減少重複程式碼:
mixin.js:
export default function (form) {
return {
props: {
value: {
default: () => form
}
},
data () {
return {
form
}
},
watch: {
form: {
handler (value) {
this.$emit(`input`, value)
},
deep: true,
immediate: true
}
}
}
}
複製程式碼
這個 js 檔案匯出了一個函式,該函式接收一個 form 引數,並將這個引數賦值給 value prop 以及 data 中的 form 欄位並返回一個物件。
然後我們將這個 mixin 註冊進每個 Form 元件中,並且改造每個 Form 元件:
<template>
<el-form ref="form" :model="form" label-position="top">
<el-form-item label="姓名">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="姓名">
<el-radio-group v-model="form.usersex">
<el-radio :label="1">男</el-radio>
<el-radio :label="0">女</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</template>
<script>
import mixin from `./mixin`
export default {
index: 1,
title: `基礎`,
name: `Form1`,
mixins: [
mixin({
username: ``,
usersex: 1
})
]
}
</script>
複製程式碼
這樣每個 Form 元件都節省下了十幾行程式碼,關鍵是這些程式碼是重複冗餘的。
最後頁面元件是這個樣子:
<template>
<d2-container>
<template slot="header">
可配置問卷示例
</template>
<div class="questionnaire">
<el-container>
<aside-left
:all="formListUseful"
:selected="forms"
@select="handleAsideSelect"
@remove="handleAsideRemove"/>
<el-main class="questionnaire__main">
<div class="questionnaire__container">
<el-card
v-for="(form, index) in forms"
:key="index"
shadow="never"
class="questionnaire__card">
<template slot="header">
{{form.title}}
</template>
<div style="margin-bottom: -20px;">
<component
:is="form.name"
v-model="forms[index].data"/>
</div>
</el-card>
</div>
</el-main>
<aside-right :res="res"/>
</el-container>
</div>
<template slot="footer">
從左側選擇要新增的表單塊,右側檢視結果
</template>
</d2-container>
</template>
<script>
import sortby from `lodash.sortby`
const req = context => context.keys().map(context)
const forms = req(require.context(`./components/Form/`, false, /.vue$/))
const components = {}
const formList = []
sortby(forms.map(e => {
const component = e.default
const { index, title, name } = component
return { component, title, index, name }
}), [`index`]).forEach(form => {
const { component, title, name } = form
components[name] = component
formList.push({ title, name })
})
export default {
name: `page1`,
components: {
...components,
AsideLeft: () => import(`./components/AsideLeft`),
AsideRight: () => import(`./components/AsideRight`)
},
data () {
return {
formList,
forms: []
}
},
computed: {
// 合併最後結果
res () {
return Object.assign({}, ...this.forms.map(e => e.data))
},
formListUseful () {
return this.formList.filter(e => !this.forms.find(f => f.name === e.name))
}
},
methods: {
handleAsideSelect (val) {
this.forms.push({
...val
})
},
handleAsideRemove (index) {
this.forms.splice(index, 1)
}
}
}
</script>
<style lang="scss">
@import `~@/assets/style/public.scss`;
.questionnaire {
@extend %full;
.el-container {
@extend %full;
}
.questionnaire__aside--left {
border-right: 1px solid #cfd7e5;
padding: 20px;
}
.questionnaire__aside--right {
border-left: 1px solid #cfd7e5;
padding: 20px;
.questionnaire__res-key {
font-size: 12px;
line-height: 14px;
color: $color-text-sub;
}
.questionnaire__res-value {
font-size: 14px;
line-height: 20px;
color: $color-text-normal;
margin-bottom: 10px;
}
}
.questionnaire__main {
background-color: rgba(#000, .05);
}
.questionnaire__container {
max-width: 400px;
margin: 0px auto;
.questionnaire__card {
border: 1px solid #cfd7e5;
margin-bottom: 20px;
.el-form-item__label {
line-height: 16px;
}
}
}
}
</style>
複製程式碼
左側頁面元件
左側右側元件不是重點內容,所以一次性展示出帶有註釋的程式碼
新建 page1/components/AsideLeft/index.vue
作為左側頁面元件
<template>
<el-aside
width="200px"
class="questionnaire__aside--left">
<!-- 已經選擇的區塊列表 點選每個按鈕後開始刪除響應的區塊 -->
<div
v-for="(item, index) in selected"
:key="index"
class="d2-mb-10">
<el-button
@click="handleRemove(item, index)"
style="width: 100%;">
{{item.title}}
</el-button>
</div>
<!-- 新建區塊按鈕 -->
<div>
<el-button
type="primary"
style="width: 100%;"
@click="dialogVisible = true">
<d2-icon name="plus"/> 新增
</el-button>
</div>
<!-- 選擇區塊介面 -->
<el-dialog
title="選擇區塊"
:append-to-body="true"
:close-on-click-modal="false"
:visible.sync="dialogVisible">
<p class="d2-mt-0">區塊標題</p>
<el-input v-model="title"></el-input>
<p>區塊元件</p>
<el-alert
v-if="all.length === 0"
type="error"
title="沒有可用區塊"/>
<el-radio-group
v-else
v-model="name"
size="small">
<el-radio-button
v-for="(item, index) in all"
:key="index"
:label="item.name">
{{item.title}}
</el-radio-button>
</el-radio-group>
<span slot="footer">
<el-button
@click="dialogVisible = false">
取 消
</el-button>
<!-- 如果沒有區塊可用 不顯示確定按鈕 -->
<el-button
v-if="all.length !== 0"
type="primary"
@click="handleSelect">
確 定
</el-button>
</span>
</el-dialog>
</el-aside>
</template>
<script>
export default {
name: `AsideLeft`,
data () {
return {
// 新建區塊的 dialog 顯示控制
dialogVisible: false,
// 新建區塊時設定的區塊標題
title: `新區塊`,
// 新建區塊時選擇的區塊
name: ``
}
},
props: {
// 所有可選區塊
all: {
default: () => []
},
// 使用者已經選擇的區塊
selected: {
default: () => []
}
},
watch: {
// 使用者選擇一個區塊後,標題自動改為這個區塊的預設標題
name (value) {
this.title = this.all.find(e => e.name === value).title
}
},
methods: {
// 使用者選擇區塊完畢
handleSelect () {
// 關閉 dialog
this.dialogVisible = false
// 傳送事件
this.$emit(`select`, {
name: this.name,
title: this.title,
data: {}
})
},
// 使用者刪除區塊
handleRemove (item, index) {
this.$confirm(`刪除 "${item.title}" 區塊嗎`, `確認操作`, {
confirmButtonText: `確定`,
cancelButtonText: `取消`,
type: `warning`
}).then(() => {
// 傳送事件
this.$emit(`remove`, index)
}).catch(() => {
this.$message({
type: `info`,
message: `已取消刪除`
})
})
}
}
}
</script>
複製程式碼
右側頁面元件
左側右側元件不是重點內容,所以一次性展示出帶有註釋的程式碼
新建 page1/components/AsideRight/index.vue
作為右側頁面元件
<template>
<el-aside
width="200px"
class="questionnaire__aside--right">
<div
v-for="(item, index) in reslist"
:key="index">
<div
class="questionnaire__res-key">
{{item.keyName}}
</div>
<div
class="questionnaire__res-value">
{{item.value === `` ? `未填寫` : item.value}}
</div>
</div>
</el-aside>
</template>
<script>
export default {
props: {
// 接收表單結果
res: {
default: () => ({})
}
},
computed: {
// 處理資料格式
reslist () {
return Object.keys(this.res).map(keyName => ({
keyName,
value: this.res[keyName]
}))
}
}
}
</script>
複製程式碼
所有程式碼就結束了,其實我們就寫了五個檔案
- 頁面元件
- 兩個側邊欄
- 表單區塊
- 表單區塊 mixin
這是一個很小但是涉及知識還不算少的小例子,如果上面的程式碼你有疑惑,可以來 D2 Projects 的 QQ 交流群 806395827 提問。
本文首發於 D2 開源專案組官方公眾號 D2 Projects
參考
地址 | 描述 |
---|---|
掘金專欄 | 掘金專欄 |
團隊主頁 | 開源團隊主頁 |
D2Admin 中文文件 | 中文文件 |
D2Admin 預覽地址 | 完整版 預覽地址 |
D2Admin github | 完整版 Github 倉庫 |
ElementUI | ElementUI 元件庫 |