功能
- 可配置型表單,通過json對象的方式自動(dòng)生成表單
- 具備更完善的功能:表單驗(yàn)證、自定義驗(yàn)證規(guī)則、動(dòng)態(tài)刪減表單、集成第三方的插件
- 用法簡單,擴(kuò)展性強(qiáng),可維護(hù)性強(qiáng)
- 能夠用在更多的場景,比如彈框嵌套表單
準(zhǔn)備工作
- 分析
element-plus表單能夠在那些方面做優(yōu)化 - 完善封裝表單的類型,支持ts
- 封裝的表單要具備
element-plus原表單的所有功能 - 集成第三方插件:markdown編輯器、富文本編輯器(
比如WangEditor)
必備UI組件
將用到的組件:
很多,涉及到表單的所有組件。
第三方組件:WangEditor
組件設(shè)計(jì)
新建src\components\baseline\form\index.ts
import { App } from 'vue'
import Form from './src/index.vue'
export { Form }
//組件可通過use的形式使用
export default {
Form,
install(app: App) {
app.component('bs-form', Form)
},
}
調(diào)整src\components\baseline\index.ts
import { App } from 'vue'
import ChooseArea from './chooseArea'
import ChooseIcon from './chooseIcon'
import Container from './container'
import Trend from './trend'
import Notification from './notification'
import List from './list'
import Menu from './menu'
import Progress from './progress'
import ChooseTime from './chooseTime'
import ChooseDate from './chooseDate'
import ChooseCity from './chooseCity'
import Form from './form'
const components = [
ChooseArea,
ChooseIcon,
Container,
Trend,
Notification,
List,
Menu,
Progress,
ChooseTime,
ChooseDate,
ChooseCity,
Form,
]
export {
ChooseArea,
ChooseIcon,
Container,
Trend,
Notification,
List,
Menu,
Progress,
ChooseTime,
ChooseDate,
ChooseCity,
Form,
}
//組件可通過use的形式使用
export default {
install(app: App) {
components.map(item => {
app.use(item)
})
},
ChooseArea,
ChooseIcon,
Container,
Trend,
Notification,
List,
Menu,
Progress,
ChooseTime,
ChooseDate,
ChooseCity,
Form,
}
路由增加,調(diào)整src\router\index.ts
{
path: '/form',
component: () => import('../views/baseline/form/index.vue'),
},
新增src\views\baseline\form\index.vue
<template>
<div class="bs-wrapper">
<bs-form :options="options"></bs-form>
</div>
</template>
<script lang="ts" setup>
import { FormOptions } from '@/components/baseline/form/src/types/types'
let options: FormOptions[] = [
{
type: 'input',
value: '',
label: '用戶名',
rules: [
{
required: true,
message: '用戶名不能為空',
trigger: 'blur',
},
{
min: 2,
max: 10,
message: '用戶名長度在2-10位之間',
trigger: 'blur',
},
],
attrs: {
showPassword: true,
},
},
]
</script>
<style lang="scss" scoped></style>
到此,基本結(jié)構(gòu)搭建完畢。
如果需要做到表單通過json自動(dòng)配置組合,需要建立一套完整的ts類型限制。
新建src\components\baseline\form\src\types\types.ts
import { CSSProperties } from 'vue'
import { RuleItem } from './rule'
import { ValidateFieldsError } from 'async-validator'
interface Callback {
(isValid?: boolean, invalidFields?: ValidateFieldsError): void
}
/**
* 表單每一項(xiàng)的配置選項(xiàng)
*/
export interface FormOptions {
// 表單項(xiàng)顯示的元素
type:
| 'cascader'//級聯(lián)選擇器
| 'checkbox'//多選框
| 'checkbox-group'
| 'checkbox-button'
| 'color-picker'
| 'date-picker'
| 'input'
| 'input-number'
| 'radio'
| 'radio-group'
| 'radio-button'
| 'rate'
| 'select'
| 'option'
| 'slider'
| 'switch'
| 'time-picker'
| 'time-select'
| 'transfer'//穿梭框
| 'upload'
| 'editor'
// 表單項(xiàng)的值
value?: any
// 表單項(xiàng)label
label?: string
// 表單項(xiàng)的標(biāo)識
prop?: string
// 表單項(xiàng)的驗(yàn)證規(guī)則
rules?: RuleItem[]//基于async-validator規(guī)則驗(yàn)證
// 表單項(xiàng)的占位符
placeholder?: string
// 表單元素特有的屬性
attrs?: {
// css樣式
style?: CSSProperties
clearable?: boolean
showPassword?: boolean
disabled?: boolean
}
// 表單項(xiàng)的子元素
children?: FormOptions[]
// 處理上傳組件的屬性和方法
uploadAttrs?: {
action: string
headers?: object
method?: 'post' | 'put' | 'patch'
multiple?: boolean
data?: any
name?: string
withCredentials?: boolean
showFileList?: boolean
drag?: boolean
accept?: string
thumbnailMode?: boolean
fileList?: any[]
listType?: 'text' | 'picture' | 'picture-card'
autoUpload?: boolean
disabled?: boolean
limit?: number
}
}
export interface ValidateFieldCallback {
(message?: string, invalidFields?: ValidateFieldsError): void
}
export interface FormInstance {
registerLabelWidth(width: number, oldWidth: number): void
deregisterLabelWidth(width: number): void
autoLabelWidth: string | undefined
emit: (evt: string, ...args: any[]) => void
labelSuffix: string
inline?: boolean
model?: Record<string, unknown>
size?: string
showMessage?: boolean
labelPosition?: string
labelWidth?: string
rules?: Record<string, unknown>
statusIcon?: boolean
hideRequiredAsterisk?: boolean
disabled?: boolean
validate: (callback?: Callback) => Promise<boolean>
resetFields: () => void
clearValidate: (props?: string | string[]) => void
validateField: (props: string | string[], cb: ValidateFieldCallback) => void
}
新建src\components\baseline\form\src\types\rule.ts
該文件從async-validator規(guī)則驗(yàn)證github項(xiàng)目中復(fù)制出來的,不需要自己思考文件內(nèi)容。
export type RuleType =
| 'string'
| 'number'
| 'boolean'
| 'method'
| 'regexp'
| 'integer'
| 'float'
| 'array'
| 'object'
| 'enum'
| 'date'
| 'url'
| 'hex'
| 'email'
| 'pattern'
| 'any';
export interface ValidateOption {
// whether to suppress internal warning
suppressWarning?: boolean;
// when the first validation rule generates an error stop processed
first?: boolean;
// when the first validation rule of the specified field generates an error stop the field processed, 'true' means all fields.
firstFields?: boolean | string[];
messages?: Partial<ValidateMessages>;
/** The name of rules need to be trigger. Will validate all rules if leave empty */
keys?: string[];
error?: (rule: InternalRuleItem, message: string) => ValidateError;
}
export type SyncErrorType = Error | string;
export type SyncValidateResult = boolean | SyncErrorType | SyncErrorType[];
export type ValidateResult = void | Promise<void> | SyncValidateResult;
export interface RuleItem {
type?: RuleType; // default type is 'string'
required?: boolean;
pattern?: RegExp | string;
min?: number; // Range of type 'string' and 'array'
max?: number; // Range of type 'string' and 'array'
len?: number; // Length of type 'string' and 'array'
enum?: Array<string | number | boolean | null | undefined>; // possible values of type 'enum'
whitespace?: boolean;
trigger?: string | string[];
fields?: Record<string, Rule>; // ignore when without required
options?: ValidateOption;
defaultField?: Rule; // 'object' or 'array' containing validation rules
transform?: (value: Value) => Value;
message?: string | ((a?: string) => string);
asyncValidator?: (
rule: InternalRuleItem,
value: Value,
callback: (error?: string | Error) => void,
source: Values,
options: ValidateOption,
) => void | Promise<void>;
validator?: (
rule: InternalRuleItem,
value: Value,
callback: (error?: string | Error) => void,
source: Values,
options: ValidateOption,
) => SyncValidateResult | void;
}
export type Rule = RuleItem | RuleItem[];
export type Rules = Record<string, Rule>;
/**
* Rule for validating a value exists in an enumerable list.
*
* @param rule The validation rule.
* @param value The value of the field on the source object.
* @param source The source object being validated.
* @param errors An array of errors that this rule may add
* validation errors to.
* @param options The validation options.
* @param options.messages The validation messages.
* @param type Rule type
*/
export type ExecuteRule = (
rule: InternalRuleItem,
value: Value,
source: Values,
errors: string[],
options: ValidateOption,
type?: string,
) => void;
/**
* Performs validation for any type.
*
* @param rule The validation rule.
* @param value The value of the field on the source object.
* @param callback The callback function.
* @param source The source object being validated.
* @param options The validation options.
* @param options.messages The validation messages.
*/
export type ExecuteValidator = (
rule: InternalRuleItem,
value: Value,
callback: (error?: string[]) => void,
source: Values,
options: ValidateOption,
) => void;
// >>>>> Message
type ValidateMessage<T extends any[] = unknown[]> =
| string
| ((...args: T) => string);
type FullField = string | undefined;
type EnumString = string | undefined;
type Pattern = string | RegExp | undefined;
type Range = number | undefined;
type Type = string | undefined;
export interface ValidateMessages {
default?: ValidateMessage;
required?: ValidateMessage<[FullField]>;
enum?: ValidateMessage<[FullField, EnumString]>;
whitespace?: ValidateMessage<[FullField]>;
date?: {
format?: ValidateMessage;
parse?: ValidateMessage;
invalid?: ValidateMessage;
};
types?: {
string?: ValidateMessage<[FullField, Type]>;
method?: ValidateMessage<[FullField, Type]>;
array?: ValidateMessage<[FullField, Type]>;
object?: ValidateMessage<[FullField, Type]>;
number?: ValidateMessage<[FullField, Type]>;
date?: ValidateMessage<[FullField, Type]>;
boolean?: ValidateMessage<[FullField, Type]>;
integer?: ValidateMessage<[FullField, Type]>;
float?: ValidateMessage<[FullField, Type]>;
regexp?: ValidateMessage<[FullField, Type]>;
email?: ValidateMessage<[FullField, Type]>;
url?: ValidateMessage<[FullField, Type]>;
hex?: ValidateMessage<[FullField, Type]>;
};
string?: {
len?: ValidateMessage<[FullField, Range]>;
min?: ValidateMessage<[FullField, Range]>;
max?: ValidateMessage<[FullField, Range]>;
range?: ValidateMessage<[FullField, Range, Range]>;
};
number?: {
len?: ValidateMessage<[FullField, Range]>;
min?: ValidateMessage<[FullField, Range]>;
max?: ValidateMessage<[FullField, Range]>;
range?: ValidateMessage<[FullField, Range, Range]>;
};
array?: {
len?: ValidateMessage<[FullField, Range]>;
min?: ValidateMessage<[FullField, Range]>;
max?: ValidateMessage<[FullField, Range]>;
range?: ValidateMessage<[FullField, Range, Range]>;
};
pattern?: {
mismatch?: ValidateMessage<[FullField, Value, Pattern]>;
};
}
export interface InternalValidateMessages extends ValidateMessages {
clone: () => InternalValidateMessages;
}
// >>>>> Values
export type Value = any;
export type Values = Record<string, Value>;
// >>>>> Validate
export interface ValidateError {
message?: string;
fieldValue?: Value;
field?: string;
}
export type ValidateFieldsError = Record<string, ValidateError[]>;
export type ValidateCallback = (
errors: ValidateError[] | null,
fields: ValidateFieldsError | Values,
) => void;
export interface RuleValuePackage {
rule: InternalRuleItem;
value: Value;
source: Values;
field: string;
}
export interface InternalRuleItem extends Omit<RuleItem, 'validator'> {
field?: string;
fullField?: string;
fullFields?: string[];
validator?: RuleItem['validator'] | ExecuteValidator;
}
組件完善
新建src\components\baseline\form\src\index.vue
<template>
<div>
<el-form>
<el-form-item v-for="(item, index) in options" :key="index">
<component :is="`el-${item.type}`"></component>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import { FormOptions } from './types/types'
let props = defineProps({
options: {
type: Array as PropType<FormOptions[]>,
required: true,
},
})
</script>
<style lang="scss" scoped></style>
運(yùn)行效果如下:

優(yōu)化:
<el-form-item
:label="item.label"
v-for="(item, index) in options"
:key="index"
>
<component :is="`el-${item.type}`"></component>
</el-form-item>
效果如下:

表單本身屬性擴(kuò)展:
<el-form v-bind="$attrs">
<el-form-item
:label="item.label"
v-for="(item, index) in options"
:key="index"
>
<component :is="`el-${item.type}`"></component>
</el-form-item>
</el-form>
修改src\views\baseline\form\index.vue
<template>
<div class="bs-wrapper">
<bs-form label-width="1rem" :options="options"></bs-form>
</div>
</template>
<script lang="ts" setup>
import { FormOptions } from '@/components/baseline/form/src/types/types'
let options: FormOptions[] = [
{
type: 'input',
value: '',
label: '用戶名',
rules: [
{
required: true,
message: '用戶名不能為空',
trigger: 'blur',
},
{
min: 2,
max: 10,
message: '用戶名長度在2-10位之間',
trigger: 'blur',
},
],
attrs: {
showPassword: true,
},
},
{
type: 'input',
value: '',
label: '密碼',
rules: [
{
required: true,
message: '密碼不能為空',
trigger: 'blur',
},
{
min: 6,
max: 20,
message: '密碼長度在6-20位之間',
trigger: 'blur',
},
],
attrs: {
showPassword: true,
},
},
]
</script>
<style lang="scss" scoped></style>
效果如下:

這里需要用到深拷貝,建議使用一個(gè)很好地第三方j(luò)s工具庫:
npm i -S lodash @types/lodash
優(yōu)化src\components\baseline\form\src\index.vue
<template>
<div>
<!-- validate-on-rule-change="false"不需要一進(jìn)來就驗(yàn)證 -->
<el-form
:validate-on-rule-change="false"
:model="model"
:rules="rules"
v-bind="$attrs"
>
<el-form-item
:prop="item.prop"
:label="item.label"
v-for="(item, index) in options"
:key="index"
>
<component
v-bind="item.attrs"
:is="`el-${item.type}`"
v-model="model[item.prop!]"
></component>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { PropType, ref, onMounted } from 'vue'
import { FormOptions } from './types/types'
let props = defineProps({
options: {
type: Array as PropType<FormOptions[]>,
required: true,
},
})
//局部引入,深拷貝
import cloneDeep from 'lodash/cloneDeep'
const model = ref<any>({})
const rules = ref<any>({})
onMounted(() => {
let m: any = {}
let r: any = {}
props.options.map((item: FormOptions) => {
m[item.prop!] = item.value
r[item.prop!] = item.rules
})
model.value = cloneDeep(m)
rules.value = cloneDeep(r)
console.log('model', model.value)
console.log('rules', rules.value)
})
</script>
<style lang="scss" scoped></style>
調(diào)整src\views\baseline\form\index.vue
<template>
<div class="bs-wrapper">
<bs-form label-width="1rem" :options="options"></bs-form>
</div>
</template>
<script lang="ts" setup>
import { FormOptions } from '@/components/baseline/form/src/types/types'
let options: FormOptions[] = [
{
type: 'input',
value: '',
label: '用戶名',
prop: 'username',
rules: [
{
required: true,
message: '用戶名不能為空',
trigger: 'blur',
},
{
min: 2,
max: 10,
message: '用戶名長度在2-10位之間',
trigger: 'blur',
},
],
attrs: {
clearable: true,
},
},
{
type: 'input',
value: '',
label: '密碼',
prop: 'password',
rules: [
{
required: true,
message: '密碼不能為空',
trigger: 'blur',
},
{
min: 6,
max: 20,
message: '密碼長度在6-20位之間',
trigger: 'blur',
},
],
attrs: {
showPassword: true,
clearable: true,
},
},
]
</script>
<style lang="scss" scoped></style>
效果基本完成:

子元素組件
像select,是存在option子元素的,調(diào)整如下:
修改src\views\baseline\form\index.vue
<template>
<div class="bs-wrapper">
<bs-form label-width="1rem" :options="options"></bs-form>
</div>
</template>
<script lang="ts" setup>
import { FormOptions } from '@/components/baseline/form/src/types/types'
let options: FormOptions[] = [
{
type: 'input',
value: '',
label: '用戶名',
prop: 'username',
placeholder: '請輸入用戶名',
rules: [
{
required: true,
message: '用戶名不能為空',
trigger: 'blur',
},
{
min: 2,
max: 10,
message: '用戶名長度在2-10位之間',
trigger: 'blur',
},
],
attrs: {
clearable: true,
},
},
{
type: 'input',
value: '',
label: '密碼',
prop: 'password',
placeholder: '請輸入6-20位密碼',
rules: [
{
required: true,
message: '密碼不能為空',
trigger: 'blur',
},
{
min: 6,
max: 20,
message: '密碼長度在6-20位之間',
trigger: 'blur',
},
],
attrs: {
showPassword: true,
clearable: true,
},
},
{
type: 'select',
value: '1',//初始化表單數(shù)據(jù)
label: '職位',
prop: 'role',
placeholder: '請選擇職位',
rules: [
{
required: true,
message: '職位不能為空',
trigger: 'blur',
},
],
children: [
{ type: 'option', label: '經(jīng)理', value: '1' },
{ type: 'option', label: '主管', value: '2' },
{ type: 'option', label: '員工', value: '3' },
],
},
]
</script>
<style lang="scss" scoped></style>
修改src\components\baseline\form\src\index.vue
<template>
<div class="bs-wrapper">
<bs-form label-width="1rem" :options="options"></bs-form>
</div>
</template>
<script lang="ts" setup>
import { FormOptions } from '@/components/baseline/form/src/types/types'
let options: FormOptions[] = [
{
type: 'input',
value: '',
label: '用戶名',
prop: 'username',
placeholder: '請輸入用戶名',
rules: [
{
required: true,
message: '用戶名不能為空',
trigger: 'blur',
},
{
min: 2,
max: 10,
message: '用戶名長度在2-10位之間',
trigger: 'blur',
},
],
attrs: {
clearable: true,
},
},
{
type: 'input',
value: '',
label: '密碼',
prop: 'password',
placeholder: '請輸入6-20位密碼',
rules: [
{
required: true,
message: '密碼不能為空',
trigger: 'blur',
},
{
min: 6,
max: 20,
message: '密碼長度在6-20位之間',
trigger: 'blur',
},
],
attrs: {
showPassword: true,
clearable: true,
},
},
{
type: 'select',
value: '1',//初始化表單數(shù)據(jù)
label: '職位',
prop: 'role',
placeholder: '請選擇職位',
rules: [
{
required: true,
message: '職位不能為空',
trigger: 'blur',
},
],
children: [
{ type: 'option', label: '經(jīng)理', value: '1' },
{ type: 'option', label: '主管', value: '2' },
{ type: 'option', label: '員工', value: '3' },
],
},
]
</script>
<style lang="scss" scoped></style>
效果如下:

style樣式增加
修改src\views\baseline\form\index.vue
<template>
<div class="bs-wrapper">
<bs-form label-width="1rem" :options="options"></bs-form>
</div>
</template>
<script lang="ts" setup>
import { FormOptions } from '@/components/baseline/form/src/types/types'
let options: FormOptions[] = [
{
type: 'input',
value: '',
label: '用戶名',
prop: 'username',
placeholder: '請輸入用戶名',
rules: [
{
required: true,
message: '用戶名不能為空',
trigger: 'blur',
},
{
min: 2,
max: 10,
message: '用戶名長度在2-10位之間',
trigger: 'blur',
},
],
attrs: {
clearable: true,
},
},
{
type: 'input',
value: '',
label: '密碼',
prop: 'password',
placeholder: '請輸入6-20位密碼',
rules: [
{
required: true,
message: '密碼不能為空',
trigger: 'blur',
},
{
min: 6,
max: 20,
message: '密碼長度在6-20位之間',
trigger: 'blur',
},
],
attrs: {
showPassword: true,
clearable: true,
},
},
{
type: 'select',
value: '1', //初始化表單數(shù)據(jù)
label: '職位',
prop: 'role',
placeholder: '請選擇職位',
rules: [
{
required: true,
message: '職位不能為空',
trigger: 'blur',
},
],
children: [
{ type: 'option', label: '經(jīng)理', value: '1' },
{ type: 'option', label: '主管', value: '2' },
{ type: 'option', label: '員工', value: '3' },
],
attrs: {
style: {
width: '100%',
},
},
},
{
type: 'checkbox-group',
value: [],
prop: 'like',
label: '愛好',
rules: [
{
required: true,
message: '愛好不能為空',
trigger: 'blur',
},
],
children: [
{
type: 'checkbox',
label: '足球',
value: '1',
},
{
type: 'checkbox',
label: '籃球',
value: '1',
},
{
type: 'checkbox',
label: '乒乓球',
value: '3',
},
],
},
{
type: 'radio-group',
value: '',
prop: 'gender',
label: '性別',
rules: [
{
required: true,
message: '性別不能為空',
trigger: 'blur',
},
],
children: [
{
type: 'radio',
label: '男',
value: '1',
},
{
type: 'radio',
label: '女',
value: '2',
},
{
type: 'radio',
label: '保密',
value: '3',
},
],
},
]
</script>
<style lang="scss" scoped></style>
修改src\components\baseline\form\src\index.vue
<template>
<div>
<!-- validate-on-rule-change="false"不需要一進(jìn)來就驗(yàn)證 -->
<el-form
ref="form"
v-if="model"
:validate-on-rule-change="false"
:model="model"
:rules="rules"
v-bind="$attrs"
>
<template v-for="(item, index) in options" :key="index">
<el-form-item
:prop="item.prop"
:label="item.label"
v-if="!item.children || !item.children!.length"
>
<component
:placeholder="item.placeholder"
v-bind="item.attrs"
:is="`el-${item.type}`"
v-model="model[item.prop!]"
></component>
</el-form-item>
<el-form-item
:prop="item.prop"
:label="item.label"
v-if="item.children && item.children.length"
>
<component
v-bind="item.attrs"
:placeholder="item.placeholder"
:is="`el-${item.type}`"
v-model="model[item.prop!]"
>
<component
v-for="(child, i) in item.children"
:key="i"
:label="child.label"
:value="child.value"
:is="`el-${child.type}`"
></component>
</component>
</el-form-item>
</template>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { PropType, ref, onMounted, watch } from 'vue'
import { FormOptions } from './types/types'
let props = defineProps({
options: {
type: Array as PropType<FormOptions[]>,
required: true,
},
})
//局部引入,深拷貝
import cloneDeep from 'lodash/cloneDeep'
const model = ref<any>()
const rules = ref<any>()
const initForm = () => {
let m: any = {}
let r: any = {}
props.options.map((item: FormOptions) => {
m[item.prop!] = item.value
r[item.prop!] = item.rules
})
model.value = cloneDeep(m)
rules.value = cloneDeep(r)
console.log('model', model.value)
console.log('rules', rules.value)
}
onMounted(() => {
if (props.options && props.options.length) {
initForm()
}
})
//監(jiān)聽父組件傳遞進(jìn)來的options的變化
watch(
() => props.options,
val => {
initForm()
},
{ deep: true }
)
</script>
<style lang="scss" scoped></style>
效果如下:
