Skip to content

表单验证

表单提交的方式,有的时候除了鼠标点击事件,还可以通过很多其他的方式来提交表单,比如键盘回车等来实现,所以使用form标签的onSubmit来提交表单。由于form表单提交会触发页面刷新,需要阻止默认事件。由于直接formData的数据,显示的是vue的复杂对象,需要toRaw将其转为简单对象。

设计表单验证的方式一般是制定一个规则,一般是数组或对象的形式来展现,然后错误的数据则以数组的形式来展示。

typescript
// 数组的形式
const rules = [
  { key: 'name', required: true, message: '必填' },
  { key: 'name', pattern: /^.{1,4}$/, message: '只能填 1 到 4 个字符' },
  { key: 'sign', required: true, message: '必填'},
]
// 对象的形式
const rules = {
  name: [
    { required: true, message: '必填' },
    { pattern: /^.{1,4}$/, message: '只能填 1 到 4 个字符' }
  ],
  sign: [
    { required: true, message:'必填' }
  ]
}
const errors = validate(formData, rules)
errors = {
  name: ['错误1', '错误2'],
  sign: ['错误3', '错误4'],
}

这里采用了数组的形式来设计表单验证方式。type的定义类型的方式不支持循环使用本身,interface的定义则可以。validate函数构造了使用范型的函数。

typescript
// type FData = Record<string,string | number | null | undefined | FData>
// 上面error
interface FData {
  [k: string]: string | number | null | undefined | FData
}
type Rule<T> = {
  key: keyof T
  message: string
} & (
  { type: 'required' } |
  { type: 'pattern', regex: RegExp }
)
type Rules<T> = Rule<T>[]
export type { Rules, Rule, FData }
export const validate = <T extends FData>(formData: T, rules: Rules<T>) => {
  type Errors = {
    [k in keyof T]?: string[]
  }
  const errors: Errors = {}
  rules.map(rule => {
    const { key, type, message } = rule
    const value = formData[key]
    switch (type) {
      case 'required':
        if (value === null || value === undefined || value === '') {
          errors[key] = errors[key] ?? []
          errors[key]?.push(message)
        }
        break;
      case 'pattern':
        if (value && !rule.regex.test(value.toString())) {
          errors[key] = errors[key] ?? []
          errors[key]?.push(message)
        }
        break;
      default:
        return
    }
  })
  return errors
}

在vue3中在使用reactive定义复杂结构的响应式数据,如果要对其重新赋值,会丢失其响应性,并不能直接进行赋值,会报错Cannot assign to "form" because it is a constant。但是可以使用Object.assign()的方法。

补充:Object.assgin()方法。用于合并对象,返回新的对象。

tsx
import { defineComponent, PropType, reactive, toRaw } from 'vue'
import { MainLayout } from '../../layouts/MainLayout'
import { Button } from '../../shared/Button'
import { EmojiSelect } from '../../shared/EmojiSelect'
import { Icon } from '../../shared/Icon'
import { Rules, validate } from '../../shared/validate'
import s from './TagCreate.module.scss'
export const TagCreate = defineComponent({
  setup: (props, context) => {
    const formData = reactive({
      name: '',
      sign: ''
    })
    // 可以通过 typeof 获取 formData 的键值
    const errors = reactive<{ [k in keyof typeof formData]?: string[] }>({})
    const rules: Rules<typeof formData> = [
      { key: 'name', type: 'required', message: '必填' },
      { key: 'name', type: 'pattern', regex: /^.{1,4}$/, message: '只能填 1 到 4 个字符' },
      { key: 'sign', type: 'required', message: '必填' },
    ]
    const onSubmit = (e: Event) => {
      // toRaw 返回简单对象
      console.log(toRaw(formData))
      // 将 errors 进行重置
      Object.assign(errors, {
        name: undefined,
        sign: undefined
      })
      // errors 进行赋值
      Object.assign(errors, validate(formData, rules))
      // 阻止默认事件发生
      e.preventDefault()
    }
    return () => (
      <MainLayout>{{
        title: () => '新建标签',
        icon: () => <Icon name='left' onClick={() => { }} />,
        default: () => (
          <form class={s.form} onSubmit={onSubmit}>
            <div class={s.formRow}>
              <label class={s.formLabel}>
                <span class={s.formItem_name}>标签名</span>
                <div class={s.formItem_value}>
                  <input v-model={formData.name}
                    class={[s.formItem, s.input, errors['name'] ? s.error : '']}/>
                </div>
                <div class={s.formItem_errorHint}>
                  <span>{errors['name'] ? errors['name'][0] : ' '}</span>
                </div>
              </label>
            </div>
            <div class={s.formRow}>
              <label class={s.formLabel}>
                <span class={s.formItem_name}>符号 {formData.sign}</span>
                <div class={s.formItem_value}>
                  <EmojiSelect v-model={formData.sign} 
                    class={[s.formItem, s.emojiList, errors['sign'] ? s.error : '']} />
                </div>
                <div class={s.formItem_errorHint}>
                  <span>{errors['sign'] ? errors['sign'][0] : ' '}</span>
                </div>
              </label>
            </div>
            <p class={s.tips}>记账时长按标签即可进行编辑</p>
            <div class={s.formRow}>
              <div class={s.formItem_value}>
                <Button class={[s.formItem, s.button]}>确定</Button>
              </div>
            </div>
          </form>
        )
      }}</MainLayout>
    )
  }
})

具体详细代码,见链接