foreword
Vue3 has been released for more than a year, and it has now been converted into a positive version. The Vue3 versions of Antd and element are also available. I got on the car not long after Vue3 was released. At that time, I found the Admin shelf of Vue3 in Github, and finally found the vue-vben-admin . I feel that the author has written well, the code is standardized, and the packaging Everything is perfect. At the time, Vben only had more than 1k stars, and now it has 10.3k. Although many think it is too bloated, it turns out that it is really good, and it is estimated that it will gradually increase in the future. At that time, I wanted to make a ported version of Vben's element, but I was too lazy, so I only set up a shelf and didn't do follow-up. In addition, Vben is really complicated, and its custom components are not very easy to port. Shelved until this year, recently picked it up, and transplanted the Form component including the usage of useForm.
project
The project address of my element-puls version is vele-admin . Currently, Model, Dialog, and Form components have been transplanted. Those who have used Vben should know that using the form of useForm, the component parameters in the template template can be passed less.
OK, let’s talk about it first, the Form component in Vben is more complicated to write, and various util functions are encapsulated more. When I wrote it here, I made a lot of reductions, the code was simplified a lot, and it was easier to understand.
analyze
useForm
Many components of Vben are secondary encapsulation of Antd, using the form of useFunc to process data, so that we do not need to write too many parameters in the template, and do not need to write a lot of repeated Antd components.
In the above code, useForm accepts the props parameter, and props is the properties of the Form component. Vben has added more properties of its own and has more custom functions. I will not do that much here. My props type is basically element. The form attribute of -plus, except schemas
, is basically passed directly to el-form. The schemas are used to automatically add the content components of the Form, which will be described in detail later.
- schemas form configuration properties
- model form data, you need to pass in ref or reactive reactive variables
- rules form validation rules
export interface FormProps {
schemas?: FormSchema[];
// 表单数据对象
model?: Recordable;
// 表单验证规则
rules: any;
// 行内表单模式
inline: boolean;
// 表单域标签的位置, 如果值为 left 或者 right 时,则需要设置 label-width
labelPosition: string;
// 表单域标签的宽度,例如 '50px'。 作为 Form 直接子元素的 form-item 会继承该值。 支持 auto。
labelWidth: string | number;
// 表单域标签的后缀
labelSuffix: string;
// 是否显示必填字段的标签旁边的红色星号
hideRequiredAsterisk: boolean;
// 是否显示校验错误信息
showMessage: boolean;
// 是否以行内形式展示校验信息
inlineMessage: boolean;
// 是否在输入框中显示校验结果反馈图标
statusIcon: boolean;
// 是否在 rules 属性改变后立即触发一次验证
validateOnRuleChange: boolean;
// 用于控制该表单内组件的尺寸 strin
size: string;
// 是否禁用该表单内的所有组件。 若设置为 true,则表单内组件上的 disabled 属性不再生效
disabled: boolean;
}
The main function of the useForm
function is to return a register
and some Form operation methods. These methods are consistent with the official method names of element-plus. In fact, they are also the native methods of directly calling el-form, and Vben is also the method of directly calling antd. Of course, it has some ways to customize the action form.
- setProps dynamically sets form properties
- validate validates the entire form
- resetFields resets the entire form, resets all field values to initial values and removes validation results
- clearValidate clears the form validation information for the specified field
- validateField method to validate some form fields
- scrollToField scrolls to the specified form field
There will be more methods returned here in Vben, and I have not done all of them here, because my data processing is not the same as Vben, and there are several methods that are not required. For example getFieldsValue
gets a certain attribute value of the form and setFieldsValue
sets the form attribute value, because I directly use the responsive object declared outside, so I can directly use/set formData outside, which is also related to the difference between element-plus and antd , and more details later.
There is nothing special about useForm
The returned register function needs to be passed to VeForm
when it is used, and the VeForm component instance will be passed to the register internally, and then the component instance can be used in useForm to call the internal methods of VeForm.
In fact, to be precise, I think the instance obtained by useForm is not a component instance of VeForm, but an object containing internal method properties provided by VeForm to useForm, so I changed this variable to formAction
instead of 06230620841847 in formRef
, but this doesn't matter, it doesn't hurt.
import { ref, onUnmounted, unref, watch, nextTick } from "vue";
import { FormActionType, FormProps } from "../types";
import { throwError } from "/@/utils/common/log";
import { isProdMode } from "/@/utils/env/env";
export default function useForm(props?: Partial<FormProps>) {
const formAction = ref<Nullable<FormActionType>>(null);
const loadedRef = ref<Nullable<boolean>>(false);
function register(instance: FormActionType) {
if (isProdMode()) {
// 开发环境下,组件卸载后释放内存
onUnmounted(() => {
formAction.value = null;
loadedRef.value = null;
});
}
// form 组件实例 instance 已存在
// 实际上 register 拿到的并不是 组件实例, 只是挂载了一些组件内部方法的 对象 formAction
if (unref(loadedRef) && isProdMode() && instance === unref(formAction)) {
return;
}
formAction.value = instance;
loadedRef.value = true;
// 监听 props, 若props改变了
// 则使用 form 实例调用内部的 setProps 方法将新的props设置到form组件内部
watch(
() => props,
() => {
if (props) {
instance.setProps(props);
}
},
{ immediate: true, deep: true }
);
}
async function getForm() {
const form = unref(formAction);
if (!form) {
throwError(
"The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!"
);
}
await nextTick();
return form as FormActionType;
}
const methods: FormActionType = {
async setProps(formProps: Partial<FormProps>) {
const form = await getForm();
form.setProps(formProps);
},
async validate(callback: (valid: any) => void) {
const form = await getForm();
form.validate(callback);
},
async validateField(
props: string | string[],
callback: (err: string) => void
) {
const form = await getForm();
form.validateField(props, callback);
},
async resetFields() {
const form = await getForm();
form.resetFields();
},
async clearValidate() {
const form = await getForm();
form.clearValidate();
},
async scrollToField(prop: string) {
const form = await getForm();
form.scrollToField(prop);
},
};
return { register, methods };
}
useFormEvents
This hook function provides resetFields
, clearValidate
, validate
, validateField
, scrollToField
form manipulation methods.
export interface FormActionType {
// 设置表单属性
setProps: (props: Partial<FormProps>) => void;
// 对整个表单作验证
validate: (callback: (valid: any) => void) => void;
// 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
resetFields: () => void;
// 清理指定字段的表单验证信息
clearValidate: (props?: string | string[]) => void;
// 对部分表单字段进行校验的方法
validateField: (
props: string | string[],
callback: (err: string) => void
) => void;
// 滚动到指定表单字段
scrollToField: (prop: string) => void;
}
In fact, when calling in el-form
, the instance formElRef
of 06230620841a1d is directly passed in, and then the corresponding api provided by el-from is directly called. Vben is extracted because there are many methods. In fact, it does not matter if it is written directly in VeForm.
The different resetFields
methods I have here are different from Vben. Vben manually modifies the variable formModel
that stores the form data. Here, because the el-form
of 06230620841a9b can be directly reset to the initial value, I deleted this paragraph.
The two methods of clearing the inspection information and inspecting the data directly adjust the api of el-form. There is a big hole in the inspection data. Of course, if someone notices it, it will be fine. The inspection, it took me a long time to find it, and it was only when I finally debugged that I found that the prop was undefined, which made me mad.
import { nextTick, Ref, unref } from "vue";
import type { FormActionType, FormProps } from "../types";
export interface UseFormActionContext {
propsRef: Ref<Partial<FormProps>>;
formElRef: Ref<FormActionType>;
}
export function useFormEvents({ formElRef }: UseFormActionContext) {
async function resetFields() {
await unref(formElRef).resetFields();
nextTick(() => clearValidate());
}
async function clearValidate(name?: string | string[]) {
await unref(formElRef).clearValidate(name);
}
async function validate(callback: (valid: any) => void) {
return await unref(formElRef).validate(callback);
}
async function validateField(
prop: string | string[],
callback: (err: string) => void
) {
return await unref(formElRef).validateField(prop, callback);
}
async function scrollToField(prop: string) {
return await unref(formElRef).scrollToField(prop);
}
return { resetFields, clearValidate, validate, validateField, scrollToField };
}
VeForm
Next is the most important component VeForm, get the el-form component instance, and then pass these form operation method objects to useForm through the register event subscribed by VeForm after the
onMounted
component is mounted, and useForm then transfers the formAction object's 16230620841cef register event. methods are provided to external components.
- getBindValue collects all the parameters passed in from the outside, including received, unreceived, passed in useForm, directly passed on the VeForm component, collected and passed to el-form
- getSchema form configuration object
- The formRef el-form component instance is passed to useFormEvents to call the method provided by el-form. In Vben, it is also passed to useFormValues for form data processing and other hook functions, which I did not do here.
- setFormModel provides the set form data method for VeFormItem
- setProps provides external methods for dynamically setting form properties
- formAction The form action object provided externally
<script lang="ts" setup>
import { computed, onMounted, ref, unref, useAttrs } from "vue";
import type { Ref } from "vue";
import type { FormActionType, FormProps } from "./types";
import VeFormItem from "./components/VeFormItem.vue";
import { useFormEvents } from "./hooks/useFormEvents";
import { useFormValues } from "./hooks/useFormValues";
const attrs = useAttrs();
const emit = defineEmits(["register"]);
const props = defineProps();
const propsRef = ref<Partial<FormProps>>({});
const formRef = ref<Nullable<FormActionType>>(null);
const defaultValueRef: Recordable = {};
// 合并接收的所有参数
const getBindValue = computed<Recordable>(() => ({
...attrs,
...props,
...propsRef.value,
}));
const getSchema = computed(() => {
const { schemas } = unref(propsRef);
return schemas || [];
});
const { validate, resetFields, clearValidate } = useFormEvents({
propsRef,
formElRef: formRef as Ref<FormActionType>,
defaultValueRef,
});
const { initDefault } = useFormValues({
defaultValueRef,
getSchema,
propsRef,
});
function setFormModel(key: string, value: any) {
if (propsRef.value.model) {
propsRef.value.model[key] = value;
}
}
function setProps(formProps: Partial<FormProps>) {
propsRef.value = { ...propsRef.value, ...formProps };
}
const formAction: Partial<FormActionType> = {
setProps,
validate,
resetFields,
clearValidate,
};
// 暴露给外面的组件实例使用
defineExpose(formAction);
onMounted(() => {
emit("register", formAction);
initDefault();
});
</script>
<template>
<el-form ref="formRef" v-bind="getBindValue">
<slot name="formHeader"></slot>
<template v-for="schema in getSchema" :key="schema.field">
<VeFormItem
:schema="schema"
:formProps="propsRef"
:setFormModel="setFormModel"
>
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</VeFormItem>
</template>
<slot name="formFooter"></slot>
</el-form>
</template>
VeFormItem
In VeForm, VeFormItem is used cyclically through the form configuration object getSchema, and propsRef
, setFormModel
and the corresponding schema are passed into VeFormItem. Vben's FormItem is written in the form of jsx. I have simplified it a lot here. I feel that it is unnecessary to use it, so I just write it as a template.
- schema
In addition to the fact that there is no need to use jsx, another reason is that I don't use jsx very much. When I write the label slot, I don't know how to use jsx, so I use the template. If the label attribute of the schema is VNode, it will be inserted into the label slot of ElFormItem; if not, it will be passed to ElFormItem.
- renderComponent
renderComponent
function returns the components of the schema configuration. componentMap
is a pre-written Map that sets the common components of the form. When the user configures the render, the render is used directly. The render must be a VNode. If not, go to the componentMap through the component attribute. .
- getModelValue
getModelValue
is dynamically set two-way binding data. It is a settable computed property. When reading, it returns the corresponding property on the model on the formProps passed in by VeForm. When setting it, call the setFormModel method passed in by VeForm, which is quite It directly manipulates the model object passed in by the user when using useForm, so the model object must be the modifiable two-way binding data declared by reactive/ref
.
- formModel(formData)
The practice of data here is not quite the same as Vben. Vben is a formModel object declared internally. It configures defaultValue through schema, and provides setFieldsValue
and getFieldsValue methods to the outside. It is also because the form of element-plus directly uses v-model to bind other actual content components such as input, and I also hope to declare formData outside, and then formData can also be updated in real time, so I can directly use formData for interface use, Pass to child components or other operations. Therefore, all form data objects use an object from the inside to the inside, which is the variable formData declared outside and passed into the model attribute of useForm.
- compAttr passes the componentProps property of the schema configuration object to the actual content component.
export interface FormSchema {
// 字段属性名
field: string;
// 标签上显示的自定义内容
label: string | VNode;
component: ComponentType;
// 子组件 属性
componentProps?: object;
// 子组件
render?: VNode;
}
<script lang="ts" setup>
import { computed, ref, useAttrs } from "vue";
import { componentMap } from "../componentMap";
import { FormSchema } from "../types";
import { ElFormItem } from "element-plus";
import { isString } from "/@/utils/help/is";
const attrs = useAttrs();
const props = defineProps<{
schema: FormSchema;
formProps: Recordable;
setFormModel: (k: string, v: any) => {};
}>();
const { component, field, label } = props.schema;
const labelIsVNode = computed(() => !isString(label));
const compAttr = computed(() => ({
...props.schema.componentProps,
}));
// 内容组件的双向绑定数据
const getModelValue = computed({
get() {
return props.formProps.model[field];
},
set(value) {
props.setFormModel(field, value);
},
});
const getBindValue = computed(() => {
const value: Recordable = {
...attrs,
prop: field,
};
if (isString(label)) {
value.label = label;
}
return value;
});
function renderComponent() {
if (props.schema.render) {
return props.schema.render;
}
return componentMap.get(component);
}
</script>
<template>
<ElFormItem v-bind="getBindValue">
<template v-if="labelIsVNode" #label>
<component :is="label" />
</template>
<component
v-model="getModelValue"
v-bind="compAttr"
:is="renderComponent()"
/>
</ElFormItem>
</template>
use
Declare the form configuration object schemas, form initial data formData
, form validation rule rules
, pass in useForm; bind register
to VeForm.
import type { FormSchema } from "/@/components/VeForm/types";
import { reactive } from "vue";
const schemas: FormSchema[] = [
{
field: "name",
label: "姓名",
component: "Input",
},
{
field: "age",
label: "年纪",
component: "InputNumber",
}
]
const rules = reactive({
name: [
{
required: true,
message: "Please input name",
trigger: "blur",
},
{
min: 3,
max: 5,
message: "Length should be 3 to 5",
trigger: "blur",
},
],
age: [
{
required: true,
message: "Please input age",
trigger: "blur",
},
]
})
If the schemas and rules are extracted from the data that will not be modified much after a single declaration, the component will become more concise. For someone like me who likes minimalism, this component looks too comfortable.
import VeForm, { useForm } from "/@/components/VeForm";
import { ref } from "vue";
const formData = reactive({
name: "shellingfordly",
age: 24,
});
const { register, methods } = useForm({
model: formData,
rules,
schemas,
});
const submitForm = () => {
methods.validate((valid: any) => {
if (valid) {
console.log("submit!", valid);
} else {
console.log("error submit!", valid);
return false;
}
});
};
const resetForm = () => {
methods.resetFields()
};
const clearValidate = () => {
methods.clearValidate()
};
<template>
<VeForm @register="register" />
<ElButton @click="submitForm">submitForm</ElButton>
</template>
The schema can be dynamically set in Vben, which I did not do here. Of course, there are also some custom methods that I haven't written. I got stuck in the form validation when I transplanted it two days ago, because I didn't notice the attribute prop
at the time, and I haven't found it. I found a bug for a long time. When I finally debugged, I found that the prop was undefined. Compared to using ElForm directly, there is a value in it. Only then did I see that was written in the document. In the case of using the validate and resetFields methods, this property is required. This The story tells us to take a good look at the documentation, and the documentation is clearly written. This bug made me vomit, and I didn't want to write it later. Hahaha, I didn't add a dynamic schema and a button to submit form data. But it doesn't matter, as a whole, the function is realized. Interested friends can help me add hahahaha, and of course other components can be transplanted. Project address vele-admin .
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。