在仿 ElementPlus 组件库的开发过程中,我们已经成功打造了多个实用组件,如 Switch 组件、 Select 组件等,最后,我们将深入探索 Form 组件的实现细节。
一、什么是 Form 组件
在前端开发中,Form 组件是用于收集用户输入数据的重要工具。它就像是一个容器,将各种表单元素,如输入框(Input)、单选框(Radio)、复选框(Checkbox)、下拉框(Select)等组合在一起,方便用户进行数据的录入和提交。
(一)常见用途
- 用户注册与登录:在注册页面,用户需要通过 Form 组件输入用户名、密码、邮箱等信息来创建账户;登录时,输入账号和密码以验证身份。
- 信息编辑:比如在个人资料编辑页面,用户可以通过 Form 组件修改自己的个人信息,像联系方式、地址等。
- 数据提交:在调查问卷、反馈表单等场景中,Form 组件用于收集用户的各种回答和意见,然后将这些数据提交到服务器进行处理。
(二)关键特性
- 表单验证:确保用户输入的数据符合特定的格式和要求,例如邮箱格式正确、密码长度符合规定等。通过验证可以提高数据的准确性和有效性,减少服务器端处理无效数据的负担。
- 数据绑定:能够将表单元素的值与应用程序中的数据模型进行绑定,实现数据的双向同步。这样,当用户在表单中输入数据时,数据模型会实时更新;反之,当数据模型发生变化时,表单元素也会相应地更新显示。
- 提交与重置功能:提供提交按钮用于将表单数据发送到服务器,同时也有重置按钮可以将表单恢复到初始状态,方便用户重新输入数据。
二、实现 Form 组件
(一)组件目录
目录
components
├── Select
├── Form.vue
├── FormItem.vue
├── types.ts
├── style.css
(二)实现 Form 组件基本静态结构
- 定义
FormItem
组件的属性类型FormItemProps
,包含label
属性。 - 实现
YlFormItem
组件,可展示标签和表单字段内容,支持自定义标签显示。 - 实现
YlForm
组件作为表单容器,可容纳多个YlFormItem
组件或其他内容。 - types.ts
typescript
export interface FormItemProps{
label:string
}
- FormItem.vue
typescript
<template>
<div class="yl-form-item">
<label class="yl-form-item__label">
<slot name="label" :label="label">
{{ label }}
</slot>
</label>
<div class="yl-form-item__content">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import type { FormItemProps } from './types';
defineOptions({
name: 'YlFormItem',
})
defineProps<FormItemProps>()
</script>
- Form.vue
typescript
<template>
<form class="yl-form">
<slot></slot>
</form>>
</template>
<script setup lang="ts">
defineOptions({
name: 'YlForm'
})
</script>
(三)实现 Form 组件单个表单元素验证功能
-
类型定义扩展 :在
types.ts
中新增了多种类型定义,包括FormItemRule
、FormRules
、FormProps
、FormContext
、FormItemContext
、FormValidateFailure
等,还定义了两个注入键formContextKey
和formItemContextKey
,用于组件间的依赖注入。 -
Form.vue
功能增强 :通过provide
函数将表单的props
提供给后代组件,这些props
包含表单数据模型model
和验证规则rules
,方便子组件获取表单相关信息。 -
FormItem.vue
验证实现- 利用
inject
获取表单上下文信息。 - 定义
validateStatus
响应式对象,用于记录验证状态。 - 通过计算属性
innerValue
获取表单元素的值,itemRules
获取当前表单项的验证规则。 getTriggeredRules
函数根据触发类型筛选出对应的验证规则。validate
函数使用async - validator
库进行表单验证,根据验证结果更新validateStatus
,并在验证结束后更新加载状态。- 最后将
validate
函数通过provide
提供给后代组件。
- 利用
-
Input.vue
验证触发 :通过inject
获取FormItem
组件提供的验证上下文。在handleInput
、handleChange
、handleBlur
等事件处理函数中,调用runValidation
函数触发相应的验证操作。
安装第三方库 async-validator
bash
npm install async-validator --save
- types.ts
typescript
import type { InjectionKey } from "vue"
import type { RuleItem, ValidateError, ValidateFieldsError } from 'async-validator'
export interface FormItemProps {
//...
prop?: string
}
export interface FormItemRule extends RuleItem {
trigger?: string
}
export type FormRules = Record<string, FormItemRule[]>
export interface FormProps {
model: Record<string, any>
rules: FormRules
}
export interface FormContext extends FormProps{
}
export interface FormItemContext {
validate: (trigger?: string) => any
}
export interface FormValidateFailure {
errors: ValidateError[] | null
fields: ValidateFieldsError
}
export const formContextKey:InjectionKey<FormContext>=Symbol('formContextKey')
export const formItemContextKey: InjectionKey<FormItemContext> = Symbol('formItemContextKey')
- Form.vue
typescript
import { provide } from 'vue'
import type { FormProps } from './types'
import { formContextKey } from './types'
const props = defineProps<FormProps>()
//...
provide(formContextKey, props)
- FormItem.vue
typescript
<template>
<div
class="yl-form-item"
:class="{
'is-error': valideteStatus.state === 'error',
'is-success': valideteStatus.state === 'success',
'is-loading': valideteStatus.loading,
}"
>
//...
<div class="yl-form-item__content">
<slot :validate="validate"></slot>
<div class="yl-form-item__error-msg" v-if="validateStatus.state === 'error'">
{{ validateStatus.errorMsg }}
</div>
</div>
//...
import { inject, computed, reactive, provide } from 'vue'
import { isNil } from 'lodash-es'
import { formContextKey, formItemContextKey } from './types'
import Schema from 'async-validator'
import type { FormItemProps, FormValidateFailure, formItemContext } from './types'
const props = defineProps<FormItemProps>()
const formContext = inject(formContextKey)
const validetaStatus = reactive({
state: 'init',
errorMsg: '',
loading: false,
})
const innerValue = computed(() => {
const model = formContext?.model
if (model && props.prop && isNil(model[props.prop])) {
return model[props.prop]
} else {
return null
}
})
const itemRules = computed(() => {
const rules = formContext?.rules
if (rules && props.prop && rules[props.prop]) {
return rules[props.prop]
} else {
return []
}
})
const getTriggeredRules = (trigger?: string) => {
const rules = itemRules.value
if (rules) {
return rules.filter((rule) => {
if (!rule.trigger || !trigger) return true
return rule.trigger && rule.trigger === trigger
})
} else {
return []
}
}
const validate = (trigger?: string) => {
const modelName = props.prop
const triggeredRules = getTriggeredRules(trigger)
if (triggeredRules.length === 0) {
return true
}
if (modelName) {
const validator = new Schema({
[modelName]: triggeredRules,
})
validateStatus.loading = true
validator
.validate({ [modelName]: innerValue.value })
.then(() => {
validateStatus.state = 'success'
})
.catch((e: FormValidateFailure) => {
const { errors } = e
validateStatus.state = 'error'
validateStatus.errorMsg = errors && errors.length > 0 ? errors[0].message || '' : ''
console.log(e.errors)
})
.finally(() => {
validateStatus.loading = false
})
}
}
const context: FormItemContext = {
validate,
}
provide(formItemContextKey, context)
Input.vue
typescript
import { formItemContextKey } from '../Form/types'
import { ref, watch, computed, useAttrs, nextTick, inject } from 'vue'
const formItemContext = inject(formItemContextKey)
const runValidation = (trigger?: string) => {
formItemContext?.validate(trigger)
}
const handleInput = () => {
emits('update:modelValue', innerValue.value)
emits('input', innerValue.value)
runValidation('input')
}
const handleChange = () => {
emits('change', innerValue.value)
runValidation('change')
}
const handleBlur = (event: FocusEvent) => {
console.log('blur triggered')
isFocus.value = false
emits('blur', event)
runValidation('blur')
}
(四)完善 Form 组件表单元素验证功能
类型定义的扩展
- 为
FormContext
接口添加了addField
和removeField
方法,用于管理表单中的表单项。 - 定义了
ValidateStatusProp
接口,用于描述表单项的验证状态。 - 为
FormItemContext
、FormInstance
和FormItemInstance
接口补充了更多方法,如resetField
、clearValidate
等,以支持重置和清除验证状态的操作。 - types.ts
typescript
export interface FormContext extends FormProps {
addField: (field: FormItemContext) => void
removeField: (field: FormItemContext) => void
}
export interface ValidateStatusProp {
state: 'init' |'success' | 'error';
errorMsg: string;
loading: boolean;
}
export interface FormItemContext {
prop: string
validate: (trigger?: string) => Promise<any>
resetField(): void
clearValidate(): void
}
export interface FormInstance {
validate: () => Promise<any>
resetFields: (props?: string[]) => void
clearValidate: (props?: string[]) => void
}
export interface FormInstance {
validate: () => Promise<any>
resetFields: (props?: string[]) => void
clearValidate: (props?: string[]) => void
}
export interface FormItemInstance {
validateStatus: ValidateStatusProp
validate: (trigger?: string) => Promise<any>
resetField(): void
clearValidate(): void
}
Form.vue
功能增强
- 表单项管理 :实现了
addField
和removeField
方法,用于添加和移除表单中的表单项。 - 表单操作方法 :
resetFields
方法可重置指定表单项或所有表单项的值和验证状态。clearValidate
方法能清除指定表单项或所有表单项的验证状态。validate
方法会对所有表单项进行验证,若所有表单项验证通过则返回true
,否则返回验证错误信息。
- 提供上下文与暴露方法 :通过
provide
函数将addField
和removeField
方法提供给子组件,同时使用defineExpose
暴露validate
、resetFields
和clearValidate
方法,使父组件可以调用这些方法。 - Form.vue
typescript
<form class="yl-form">
<slot></slot>
</form>
//...
import type { ValidateFieldsError } from 'async-validator'
import type {
FormProps,
FormItemContext,
FormContext,
FormValidateFailure,
FormInstance,
} from './types'
const addField: FormContext['addField'] = (field) => {
fields.push(field)
}
const removeField: FormContext['removeField'] = (field) => {
if (field.prop) {
fields.splice(fields.indexOf(field), 1)
}
}
const resetFields = (keys: string[] = []) => {
const filterArr = keys.length > 0 ? fields.filter((field) => keys.includes(field.prop)) : fields
filterArr.forEach((field) => field.resetField())
}
const clearValidate = (keys: string[] = []) => {
const filterArr = keys.length > 0 ? fields.filter((field) => keys.includes(field.prop)) : fields
filterArr.forEach((field) => field.clearValidate())
}
const validate = async () => {
let validationErrors: ValidateFieldsError = {}
console.log('fields', fields)
for (const field of fields) {
try {
await field.validate('')
} catch (e) {
const error = e as FormValidateFailure
validationErrors = {
...validationErrors,
...error.fields,
}
}
}
if (Object.keys(validationErrors).length === 0) return true
return Promise.reject(validationErrors)
}
provide(formContextKey, {
...props,
addField,
removeField,
})
defineExpose<FormInstance>({
validate,
resetFields,
clearValidate,
})
FormItem.vue
功能完善
- 验证状态管理 :定义了
validateStatus
响应式对象,用于存储表单项的验证状态。 - 验证方法优化 :
validate
方法现在返回一个Promise
对象,可更方便地处理异步验证结果。 - 重置与清除方法 :
resetField
方法用于重置表单项的值和验证状态。clearValidate
方法用于清除表单项的验证状态。
- 表单项注册与注销 :在
onMounted
钩子中调用formContext.addField
方法将表单项注册到表单中,并记录初始值;在onUnmounted
钩子中调用formContext.removeField
方法将表单项从表单中移除。 - 暴露实例方法 :使用
defineExpose
暴露validateStatus
、validate
、resetField
和clearValidate
方法,方便父组件访问。 - FormItem.vue
typescript
<template>
<div
class="yl-form-item"
:class="{
//...
'is-required': isRequired,
}"
import { inject, computed, reactive, provide, onMounted, onUnmounted } from 'vue'
import type {
//...
ValidateStatusProp,
FormItemInstance,
} from './types'
const validateStatus: ValidateStatusProp = reactive({
state: 'init',
errorMsg: '',
loading: false,
})
let initialValue: any = null
const validate = async (trigger?: string) => {
//...
return validator
//...
.catch((e: FormValidateFailure) => {
//...
return Promise.reject(e)
})
//...
}
const clearValidate = () => {
validateStatus.state = 'init'
validateStatus.errorMsg = ''
validateStatus.loading = false
}
const resetField = () => {
clearValidate()
const model = formContext?.model
if (model && props.prop && isNil(model[props.prop])) {
model[props.prop] = initialValue
}
}
const isRequired = computed(() => {
return itemRules.value.some((rule) => rule.required)
})
const context: FormItemContext = {
validate,
prop: props.prop || '',
clearValidate,
resetField,
}
onMounted(() => {
if (props.prop) {
formContext?.addField(context)
initialValue = innerValue.value
}
})
onUnmounted(() => {
formContext?.removeField(context)
})
defineExpose<FormItemInstance>({
validateStatus,
validate,
resetField,
clearValidate,
})
Input.vue
功能改进
runValidation
方法在调用 formItemContext.validate
时添加了错误处理,捕获并打印验证错误信息。
- Input.vue
typescript
const runValidation = (trigger?: string) => {
formItemContext?.validate(trigger).catch((e)=>console.log(e.errors));
}
(五)为 Form 组件添加样式
- style.css
css
.yl-form {
--yl-form-label-font-size: var(--yl-font-size-base);
--yl-form-content-font-size: var(--yl-font-size-base);
}
.yl-form-item {
display: flex;
margin-bottom: 18px;
.yl-form-item__label {
width: 150px;
height: 32px;
line-height: 32px;
padding: 0 12px 0 0;
box-sizing: border-box;
display: inline-flex;
justify-content: flex-end;
font-size: var(--yl-form-label-font-size);
color: var(--yl-text-color-regular);
}
.yl-form-item__content {
display: flex;
flex-wrap: wrap;
align-items: center;
flex: 1;
line-height: 32px;
font-size: var(--yl-form-content-font-size);
min-width: 0;
position: relative;
}
.yl-form-item__error-msg {
position: absolute;
top: 100%;
top: 0;
padding-top: 2px;
color: var(--yl-color-danger);
font-size: 12px;
line-height: 1;
}
}
.yl-form-item.is-error.yl-input__wrapper {
box-shadow: 0 0 0 1px var(--yl-color-danger) inset;
}
.yl-form-item.is-success.yl-input__wrapper {
box-shadow: 0 0 0 1px var(--yl-color-success) inset;
}
.yl-form-item.is-required > .yl-form-item__label::before {
content: '*';
color: var(--yl-color-danger);
margin-right: 4px;
}