本案例將會講解如何使用 vue.js + ElementUI 開發一個簡單的 可配置組合表單 Demo。css
示例源代碼 githubvue
操做演示(GIF 較大):webpack
在左側新建表單區塊,選擇區塊標題和表單組件類型後點擊肯定,會在中間區域生成一個塊新的表單,右側展現了全部表單的數據合併結果。git
在本示例中你主要能夠看到如下知識點的運用:github
<component>
組件上面列舉的這些是由於之前有羣裏朋友詢問相關的實現方法,在此列出,可能正在讀這篇文章的你已經都掌握了,恭喜你!(本篇文章的原由也是羣友提問)web
下面開始正文數組
這個 demo 的全部組件和邏輯若是寫在一個文件中大概會有幾百行,維護起來會有麻煩,因此首先設計這樣的目錄結構:sass
爲了快速開發頁面本項目使用 ElementUI 和 D2Admin 快速搭建,如下示例中組件都來自這兩個開源項目,若是你不認識這些組件也沒有關係,大體瞭解意思就可。app
首先寫出頁面的大體框架:框架
<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
因此在上面代碼中
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>
複製代碼
全部代碼就結束了,其實咱們就寫了五個文件
這是一個很小可是涉及知識還不算少的小例子,若是上面的代碼你有疑惑,能夠來 D2 Projects 的 QQ 交流羣 806395827 提問。
本文首發於 D2 開源項目組官方公衆號 D2 Projects
地址 | 描述 |
---|---|
掘金專欄 | 掘金專欄 |
團隊主頁 | 開源團隊主頁 |
D2Admin 中文文檔 | 中文文檔 |
D2Admin 預覽地址 | 完整版 預覽地址 |
D2Admin github | 完整版 Github 倉庫 |
ElementUI | ElementUI 組件庫 |