仿 ElementPlus 组件库( 十一 )—— Form 组件实现

在仿 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 中新增了多种类型定义,包括 FormItemRuleFormRulesFormPropsFormContextFormItemContextFormValidateFailure 等,还定义了两个注入键 formContextKeyformItemContextKey,用于组件间的依赖注入。

  • Form.vue 功能增强 :通过 provide 函数将表单的 props 提供给后代组件,这些 props 包含表单数据模型 model 和验证规则 rules,方便子组件获取表单相关信息。

  • FormItem.vue 验证实现

    • 利用 inject 获取表单上下文信息。
    • 定义 validateStatus 响应式对象,用于记录验证状态。
    • 通过计算属性 innerValue 获取表单元素的值,itemRules 获取当前表单项的验证规则。
    • getTriggeredRules 函数根据触发类型筛选出对应的验证规则。
    • validate 函数使用 async - validator 库进行表单验证,根据验证结果更新 validateStatus,并在验证结束后更新加载状态。
    • 最后将 validate 函数通过 provide 提供给后代组件。
  • Input.vue 验证触发 :通过 inject 获取 FormItem 组件提供的验证上下文。在 handleInputhandleChangehandleBlur 等事件处理函数中,调用 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 接口添加了 addFieldremoveField 方法,用于管理表单中的表单项。
  • 定义了 ValidateStatusProp 接口,用于描述表单项的验证状态。
  • FormItemContextFormInstanceFormItemInstance 接口补充了更多方法,如 resetFieldclearValidate 等,以支持重置和清除验证状态的操作。
  • 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 功能增强

  • 表单项管理 :实现了 addFieldremoveField 方法,用于添加和移除表单中的表单项。
  • 表单操作方法
    • resetFields 方法可重置指定表单项或所有表单项的值和验证状态。
    • clearValidate 方法能清除指定表单项或所有表单项的验证状态。
    • validate 方法会对所有表单项进行验证,若所有表单项验证通过则返回 true,否则返回验证错误信息。
  • 提供上下文与暴露方法 :通过 provide 函数将 addFieldremoveField 方法提供给子组件,同时使用 defineExpose 暴露 validateresetFieldsclearValidate 方法,使父组件可以调用这些方法。
  • 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 暴露 validateStatusvalidateresetFieldclearValidate 方法,方便父组件访问。
  • 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;
}
相关推荐
Lepusarcticus12 分钟前
《掌握 JavaScript 字符串操作,这一篇就够了!》
前端·javascript
田本初17 分钟前
vue-cli工具build测试与生产包对css处理的不同
前端·css·vue.js
inxunoffice1 小时前
批量在多个 PDF 的指定位置插入页,如插入封面、插入尾页
前端·pdf
木木黄木木1 小时前
HTML5 Canvas绘画板项目实战:打造一个功能丰富的在线画板
前端·html·html5
豆芽8191 小时前
基于Web的交互式智能成绩管理系统设计
前端·python·信息可视化·数据分析·交互·web·数据可视化
不是鱼1 小时前
XSS 和 CSRF 为什么值得你的关注?
前端·javascript
顺遂时光1 小时前
微信小程序——解构赋值与普通赋值
前端·javascript·vue.js
anyeyongzhou1 小时前
img标签请求浏览器资源携带请求头
前端·vue.js
Captaincc1 小时前
腾讯云 EdgeOne Pages「MCP Server」正式发布
前端·腾讯·mcp
最新资讯动态2 小时前
想让鸿蒙应用快的“飞起”,来HarmonyOS开发者官网“最佳实践-性能专区”
前端