功能
- 可配置型表单,通过json对象的方式自动生成表单
- 具备更完善的功能:表单验证、自定义验证规则、动态删减表单、集成第三方的插件
- 用法简单,扩展性强,可维护性强
- 能够用在更多的场景,比如弹框嵌套表单
准备工作
- 分析
element-plus
表单能够在那些方面做优化 - 完善封装表单的类型,支持ts
- 封装的表单要具备
element-plus
原表单的所有功能 - 集成第三方插件:markdown编辑器、富文本编辑器(
比如WangEditor)
必备UI组件
将用到的组件:
很多,涉及到表单的所有组件。
第三方组件:WangEditor
组件设计
新建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)
},
}
调整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,
}
路由增加,调整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>
到此,基本结构搭建完毕。
如果需要做到表单通过json自动配置组合,需要建立一套完整的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
}
/**
* 表单每一项的配置选项
*/
export interface FormOptions {
// 表单项显示的元素
type:
| 'cascader'//级联选择器
| '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'
// 表单项的值
value?: any
// 表单项label
label?: string
// 表单项的标识
prop?: string
// 表单项的验证规则
rules?: RuleItem[]//基于async-validator规则验证
// 表单项的占位符
placeholder?: string
// 表单元素特有的属性
attrs?: {
// css样式
style?: CSSProperties
clearable?: boolean
showPassword?: boolean
disabled?: boolean
}
// 表单项的子元素
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规则验证github项目中复制出来的,不需要自己思考文件内容。
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>
运行效果如下:
优化:
<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 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>
效果如下:
这里需要用到深拷贝,建议使用一个很好地第三方js工具库:
npm i -S lodash @types/lodash
优化src\components\baseline\form\src\index.vue
<template>
<div>
<!-- validate-on-rule-change="false"不需要一进来就验证 -->
<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>
调整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子元素的,调整如下:
修改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',//初始化表单数据
label: '职位',
prop: 'role',
placeholder: '请选择职位',
rules: [
{
required: true,
message: '职位不能为空',
trigger: 'blur',
},
],
children: [
{ type: 'option', label: '经理', 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',//初始化表单数据
label: '职位',
prop: 'role',
placeholder: '请选择职位',
rules: [
{
required: true,
message: '职位不能为空',
trigger: 'blur',
},
],
children: [
{ type: 'option', label: '经理', 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', //初始化表单数据
label: '职位',
prop: 'role',
placeholder: '请选择职位',
rules: [
{
required: true,
message: '职位不能为空',
trigger: 'blur',
},
],
children: [
{ type: 'option', label: '经理', 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"不需要一进来就验证 -->
<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()
}
})
//监听父组件传递进来的options的变化
watch(
() => props.options,
val => {
initForm()
},
{ deep: true }
)
</script>
<style lang="scss" scoped></style>
效果如下: