需求
設計一個基於 vue 和 element ui 的多卡片組單一表單組件,卡片組用於分類若干字段,比如個人信息、職業信息、技能信息。同時,將標簽等文本抽離 HTML,方便后續增加語言模塊。
分析
原始的 <el-form>
不支持批量設置字段,當頁面中字段較多時,維護和修改 HTML 過於繁瑣;原始的數據對象和規則對象完全扁平化,且無法分組,不便於與頁面結構一 一對應,實現分層遍歷的效果。
注意:使用element ui的表單功能時,必須在
:model="data"
的形式,而在字段組件如
中,則必須使用
v-model="value"
的形式,否則,表單組件將無法綁定model,可能導致所有字段無法輸入(對於下拉框組件可能允許選擇,但選中的內容不會同步到輸入框)
常見報錯:
錯誤1:字段未綁定model,組件加載時報錯
錯誤提示:[Vue warn]: Missing required prop: "value"
錯誤2:表單未綁定model,組件加載時無提示,但使用驗證等功能時提示警告信息
錯誤提示:[Element Warn][Form]model is required for validate to work!
設計解決方案
方案 1
- 由於 HTML 是不需要動的,因此考慮將卡片的標題、表單字段的 label,model 和 rule 屬性從 HTML 中抽離出來,作為配置參數,並單獨創建一個結構化的對象(對應於卡片和字段的層級關系),存儲以上 4 類信息
- 再通過 v-for 遍歷結構化對象,並逐層生成卡片
<el-card>
和字段<el-form-item>
。
相比於直接維護 HTML,維護 JS 對象需要的代碼量少很多,且沒有無關信息的干擾(屬性綁定以及嵌套標簽等,重復的任務很容易出錯,且不易被發現)。
JS 對象結構:
// fields 下面的每個對象代表一個卡片,屬性 label 是一個對象,用於存放各字段的標簽名
fields {
personalInfo {
title: '',
lable: {
name: '',
...
}
...
}
},
// datas 對象用於存放字段綁定的數據
datas {
personalInfo {
name: '',
...
}
},
// rules 對象用於存放字段綁定的數據
rules {
personalInfo {
name: '',
...
}
}
存在的問題
方案 1 創建的結構化對象參考了 element ui ,將各信息分開存儲,導致每次修改都要改多處,並且由於使用了嵌套結構,導致定位繁瑣;且部分屬性使用的數據類型不規范,如卡片組和標簽組對象應該使用數組類型。
方案 2
統一成一個結構化對象:每個字段對象存儲 label、model 和 rule 三個信息,字段外層再嵌套卡片對象
改進后的 JS 對象結構:
// fieldGroupList 作為整個結構化對象,存放所有數據,並根據所在卡片進行分組
fieldGroupList: [
{
fieldTitle: '個人基本信息',
// fieldList 存放一個卡片內的多個字段,每個字段對象包含 label、val 和 rule 三個屬性,對應標簽名、數據和校驗規則
fieldList: [
{
key: 'name', // 用於標記一個字段
label: '姓名',
val: '',
rule: [{ required: true, message: `請輸入姓名`, trigger: 'blur' },],
},
... // 其他字段
],
},
... // 其他卡片
]
對應的 HTML:
<el-form label-width="100px">
<div class="el-card-group">
<el-card v-for="(fieldGroup, i) in fieldGroupList" :key="i">
<div slot="header">
<span>{{ fieldGroup.fieldTitle }}</span>
</div>
<el-form-item
:label="fieldItem.label"
:rules="fieldItem.rule"
v-for="(fieldItem, i) in fieldGroup.fieldList"
:key="i"
>
<el-input v-model="fieldItem.val"></el-input>
</el-form-item>
</el-card>
</div>
<div class="el-form-btn-group">
<el-button type="primary" @click="submitForm('fieldGroupList')"
>提交</el-button
>
<el-button @click="resetForm('fieldGroupList')">重置</el-button>
</div>
</el-form>
存在的問題
由於不是直接通過原始的扁平化數據對象和規則對象進行驗證,無法使用框架內置的驗證、重置等方法,需要重寫,因此還需要改進
方案3
在方案2的基礎上:當初始化組件時,將結構化對象中 對應於各字段的數據變量和規則變量 還原成 element ui 可接受的扁平化數據對象和規則對象,以便直接使用框架內置的表單驗證等方法。
data() {
return {
// 初始化數據對象和規則對象為空對象,並在 created 鈎子方法中賦予其對應的數據
ruleForm: {
datas: {},
rules: {},
},
fieldGroupList: [
// 與方案2相同的結構化對象
],
};
},
created() {
this.setRuleForm(this.fieldGroupList, this.ruleForm); // 在初始化階段調用轉換方法,避免獲取到空對象
},
methods: {
// 定義轉換方法
setRuleForm(fieldGroupList, ruleForm) {
let datas = {},
rules = {};
if (fieldGroupList) {
fieldGroupList.map((fieldGroup) => {
fieldGroup.fieldList.map((field) => {
datas[field.key] = field.val;
rules[field.key] = field.rule;
});
});
ruleForm.datas = datas;
ruleForm.rules = rules;
} else {
throw new Error('解析失敗,無法顯示表單項目');
}
},
... // 其他方法
}
對應的 HTML:
<el-form
ref="ruleForm"
:model="ruleForm.datas"
:rules="ruleForm.rules"
label-width="100px"
>
<div class="el-card-group">
<el-card v-for="(fieldGroup, i) in fieldGroupList" :key="i">
<div slot="header">
<span>{{ fieldGroup.fieldTitle }}</span>
</div>
<el-form-item
:label="fieldItem.label"
:rules="ruleForm.rules[fieldItem.key]"
:prop="fieldItem.key"
v-for="(fieldItem, i) in fieldGroup.fieldList"
:key="i"
>
<el-input v-model="ruleForm.datas[fieldItem.key]"></el-input>
</el-form-item>
</el-card>
</div>
<div class="el-form-btn-group">
<el-button type="primary" @click="submitForm('ruleForm')"
>提交</el-button
>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</div>
</el-form>
至此,已經基本滿足了需求。在保留了原組件方法的基礎上,允許自定義多個卡片和多個字段,而且后續增刪改字段也不需要動 HTML 。
目前尚未解決的問題
- 輸入字段的類型
<input type="***">
不能定制
這個問題可以通過與上述類似的方式解決,即增加一個表示輸入類型的屬性並在 HTML 中綁定。但是這種實現方式會導致組件的維護越來越復雜和困難,因為標簽擁有的屬性都需要在 JS 對象中進行定義,每次擴展都需要直接修改 JS 對象,不符合開閉原則。
稍微好一點的方法是通過單獨定義一個對象,並注入到現有對象中,這樣避免了直接修改原 JS 對象。 - 布局不能定制,比如需要某些特定字段顯示在一行,某些字段獨立成行。這些需求需要通過添加額外的
<el-row>
和<el-col>
組件來解決,因此需要同步修改 HTML 部分,現有組件不能直接復用。目前的想法是通過分離卡片<el-card>
和字段<el-form-item>
,並在中間插入代表布局的行和列組件,同時,單獨創建布局對象,存放布局相關參數並在行和列組件中綁定。同時,分離后的卡片和字段組件在其他場景下也能單獨復用;行列組件也可以通過參數化的方式批量生成。