Vben-Admin Form组件篇
注 : 关于Vben总体设计参考Vben框架设计
去年的今天我写了关于Vben第一篇文章,今天突发奇想更新下关于Vben的文章,准备发布时看到和去年日期竟然一样 0 0

本文主要讲解的是 BasicForm组件的用法及原理
用法
- 当我们引入BasicForm组件时,通常需要通过register进行挂载
- 通过其封装的useForm进行挂载,传入相应的配置,并可获得表单的一些方法能力
javascript
setup() {
const [register, { validate }] = useForm({
labelWidth: 130,
schemas: aboutSchema,
showSubmitButton: true,
showResetButton: false,
submitButtonOptions: {
text: '提交',
},
actionColOptions: {
span: 24,
},
});
const handleSubmit = async () => {
const data = await validate();
console.log(data);
};
return {
register,
handleSubmit,
};
},
render() {
return (
<Row>
<Col span={20}>
<BasicForm onRegister={this.register} onSubmit={this.handleSubmit}></BasicForm>
</Col>
</Row>
);
},
一、让我们先看看通过useForm是如何进行挂载,以及如何获取form的validate方法
- 由BasicForm中的代码可见,在其mounted时,调用了外部传入的register方法,并传入了表单实用方法
javascript
const formActionType: Partial<FormActionType> = {
getFieldsValue,
setFieldsValue,
resetFields,
updateSchema,
resetSchema,
setProps,
removeSchemaByFiled,
appendSchemaByField,
clearValidate,
validateFields,
validate,
submit: handleSubmit,
scrollToField: scrollToField,
};
onMounted(() => {
initDefault();
emit('register', formActionType);
});
- 这时调用了useForm中的register方法,并传入了formActionType
- register中做的事情也比较简单,声明了一个对象,并保存了formActionType
javascript
function register(instance: FormActionType) {
isProdMode() &&
onUnmounted(() => {
formRef.value = null;
loadedRef.value = null;
});
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
formRef.value = instance;
loadedRef.value = true;
watch(
() => props,
() => {
props && instance.setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
},
);
}
- 注意 这里监听了props,在挂载的过程中,调用了BasicForm中的setProps方法,useForm传入的配置项在这时进行初始化
- 紧接着 我们一起看下BasicForm中的setProps做了哪些事情
javascript
// Get the basic configuration of the form
const getProps = computed((): FormProps => {
return { ...props, ...unref(propsRef as FormProps) } as FormProps;
});
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
- 这里大家应该明白,通过setProps后,props通过计算属性,以getProps方式传给BasicForm下的FormItem 组件
以上讲了BasicForm主要挂载方式流程,并举例了setProps方法的实现,例如 validate、resetSchema等其他form方法,有兴趣可以看看其实现,这里不做过多分析
二、通过shcemas如何渲染出具体组件,以及自定义组件如何封装
这里通过两个例子来说明渲染组件逻辑
javascript
export const aboutSchema: FormSchema[] = [
{
label: '数量',
field: 'number',
component: 'InputNumber',
},
{
label: '愿景',
field: 'vision',
component: 'Input',
required: true,
colProps: {
span: 12,
},
render: ({ model, field }) => {
return h(Tinymce, {
value: model[field],
onChange: (value: string) => {
model[field] = value;
},
});
},
}]
- 在FormItem中,核心渲染逻辑为 renderComponent方法,这里先贴下完整的代码
javascript
function renderComponent() {
const {
renderComponentContent,
component,
field,
changeEvent = 'change',
valueField,
} = props.schema;
const isCheck = component && ['Switch', 'Checkbox'].includes(component);
const eventKey = `on${upperFirst(changeEvent)}`;
const on = {
[eventKey]: (...args: Nullable<Recordable>[]) => {
const [e] = args;
if (propsData[eventKey]) {
propsData[eventKey](...args);
}
const target = e ? e.target : null;
const value = target ? (isCheck ? target.checked : target.value) : e;
props.setFormModel(field, value);
},
};
const Comp = componentMap.get(component) as ReturnType<typeof defineComponent>;
const { autoSetPlaceHolder, size } = props.formProps;
const propsData: Recordable = {
allowClear: true,
getPopupContainer: (trigger: Element) => trigger.parentNode,
size,
...unref(getComponentsProps),
disabled: unref(getDisable),
};
const isCreatePlaceholder = !propsData.disabled && autoSetPlaceHolder;
// RangePicker place is an array
if (isCreatePlaceholder && component !== 'RangePicker' && component) {
propsData.placeholder =
unref(getComponentsProps)?.placeholder || createPlaceholderMessage(component);
}
propsData.codeField = field;
propsData.formValues = unref(getValues);
const bindValue: Recordable = {
[valueField || (isCheck ? 'checked' : 'value')]: props.formModel[field],
};
const compAttr: Recordable = {
...propsData,
...on,
...bindValue,
};
if (!renderComponentContent) {
return <Comp {...compAttr} />;
}
const compSlot = isFunction(renderComponentContent)
? { ...renderComponentContent(unref(getValues)) }
: {
default: () => renderComponentContent,
};
return <Comp {...compAttr}>{compSlot}</Comp>;
}
- 由代码可以看出 最终返回的是Comp组件,不难看出,这里是根据schemas中的component字段,通过字典的形式,去取到对应的组件
- 我们重点来看下 schemas中的field是如何进行双向绑定的,这个划重点 , 这里默认实现了自定义change事件,并对field字段进行赋值
ini
const on = {
[eventKey]: (...args: Nullable<Recordable>[]) => {
const [e] = args;
if (propsData[eventKey]) {
propsData[eventKey](...args);
}
const target = e ? e.target : null;
const value = target ? (isCheck ? target.checked : target.value) : e;
props.setFormModel(field, value);
},
};
- 关于setFormModel的实现在BascifForm中,比较简单,不做赘述
- 这里还对初始化的setProps传进来的props进行重新组合,并最终传给了 Comp 组件
- 所以当我们schema的component为Input等组件时,渲染后在修改其值时,会调用这里自定义的change事件,并完成fled字段的赋值
接下来我们来看看自定义组件双向绑定逻辑
这里用ApiSelect进行举例,其位于src -> components ->Form -> src -> components
- 映入眼帘的,通过Vue的b-bind="$attrs"进行属性透传,这里不理解的,自行看vue文档
- 紧接着 通过v-model:value="state",进行value绑定,这里有人纳闷,这state是什么玩意,不是我传进来的啊?
javascript
<template>
<Select
@dropdown-visible-change="handleFetch"
v-bind="$attrs"
@change="handleChange"
:options="getOptions"
v-model:value="state"
>
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template #suffixIcon v-if="loading">
<LoadingOutlined spin />
</template>
<template #notFoundContent v-if="loading">
<span>
<LoadingOutlined spin class="mr-1" />
{{ t('component.form.apiSelectNotFound') }}
</span>
</template>
</Select>
</template>
- 仔细一看,噢,这玩意原来是通过一个名为useRuleFormItem的hooks生成的, 那我们一起看看这个useRuleFormItem做了啥事情
javascript
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
- 好家伙,原来state是一个计算属性,并通过观察者模式,在state改变时,调用了chageEvent也就是传进来的自定义change事件
这样一看是不是逐渐脉络清晰了起来