手写简易实现一个form-item组件 | 青训营

366 阅读2分钟

Form组件的基本使用,外层是form组件,内部是form-item,两部分组成。相当于是两个组件,item是针对某一项,比如输入框进行校验,我们就可以使用form-item包裹起来,最后将所有的form-item包裹在form组件中。 我们应该先从内部开始设计组件。

  • k-form

    • 载体,输入数据model,校验规则rules
    • form实例上有校验validate函数
  • k-form-item

    • label标签添加
    • 载体,输入项包装
    • 校验执行者,显示错误

文件结构

index.ts来整合表单组件,是入口。 image.png

FormItem Props定义

  • prop:校验的输入框的属性
  • label:输入框的标题
  • rules:表单的验证规则,可参考 async-validator
  • show-message:是否显示错误,默认为true
  • change / blur事件

注:需要本地安装async-validator,常见的规则都可以从导出的RuleItem看见,我们需要在此基础上扩展

image.png

// form-item.ts
import type { RuleItem } from 'async-validator'
import { ExtractPropTypes, InjectionKey, PropType } from 'vue'

export type Arrayable<T> = T | T[]

export const formItemValidateState = ['success', 'error', ''] as const
export type FormItemValidateState = (typeof formItemValidateState)[number]

export interface FormItemRule extends RuleItem {
  trigger?: Arrayable<string>
}

export const formItemProps = {
  prop: String,
  label: String,
  rules: [Object, Array] as PropType<Arrayable<FormItemRule>>,
  showMessage: {
    type: Boolean,
    default: true
  }
} as const
// Partial把属性变为可选的
export type FormItemProps = Partial<ExtractPropTypes<typeof formItemProps>>

Form-item结构实现

form-item.vue

<template>
  <div :class="formItemClasses">
    <!-- label属性 -->
    <label :class="bem.e('label')">
      <slot name="label">
        {{ label }}
      </slot></label
    >
    <!-- content盒⼦ -->
    <div :class="bem.e('content')">
      <!-- input -->
      <slot></slot>
      <!-- 错误信息 -->
      <div :class="bem.e('error')">
        <slot name="error">
          {{ validateMessage }}
        </slot>
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { createNamespace } from '@kalin-ui/utils/create'
import { computed, ref } from 'vue'
import { formItemProps, FormItemValidateState } from './form-item'
const validateState = ref<FormItemValidateState>('error') // 校验状态
const validateMessage = ref('校验失败') // 错误消息
const bem = createNamespace('form-item')
const formItemClasses = computed(() => [
    bem.b(),
    bem.is('success', validateState.value ===
    'success'),
    bem.is('error', validateState.value ===
    'error')
])
// DefineOptions 宏定义组件选项
defineOptions({
    name: 'KFormItem'
})
const props = defineProps(formItemProps)
const shouldShowError = computed(() => {
    // 当状态为失败并且需要显示错误消息时
    return validateState.value === 'error' && props.showMessage
})
</script>

Form-item入口编写

import { withInstall } from '@kalin-ui/utils/with-install'
import _FormItem from './src/form-item.vue'

export const FormItem = withInstall(_FormItem)
export type FormInstance = InstanceType<typeof Form>

Form-item组件使用:

<script setup lang="ts">
import { ref } from 'vue'
const value = ref('')
</script>
<template>
    <k-form-item label="username">
        <k-input v-model="value"></k-input>
    </k-form-item>
</template>

FromItem校验

<k-form-item
    label="⽤户名"
    prop="username"
    :rules="[
        { required: true, message: '⽤户名必须填写', trigger: 'blur' },
        { min: 6, message: '⽤户名最少6位', trigger: 'change' }
    ]"
>
    <k-input v-model="value"></k-input>
</k-form-item>

注:我们普通使用form-item组件的时候可能在外层套了很多层,比如row、col之类的布局组件,所以属性需要跨级传递,在vue3中使用provide/inject实现。 属性中进行定义:

export interface FormItemContext extends FormItemProps {
  validate: (
    trigger: string,
    callback?: (isValid: boolean) => void
  ) => Promise<void>
}

export const FormItemContextKey: InjectionKey<FormItemContext> =
  Symbol('form-item')

form-item.vue 根据对应的trigger类型过滤规则,在validate方法中,拿到触发的时机,校验是否通过可以调用callback 或者调用promise.then方法

const _rules = computed(() => {
    const rules: FormItemRule[] = props.rules
    ? Array.isArray(props.rules)
        ? props.rules
        : [props.rules]
    : []
    
    return rules
})

const getFilteredRule = (trigger: string) => {
    const rules = _rules.value
    return rules.filter(rule => {
        if (!rule.trigger || !trigger) return true // 这种情况意味着无论如何都要校验
        if (Array.isArray(rule.trigger)) {
            return rule.trigger.includes(trigger)
        } else {
            return rule.trigger == trigger
        }
    })
}

const validate: FormItemContext['validate'] = async(trigger, callback?) => {
    const rules = getFilteredRule(trigger) 
    console.log('校验', rules)
}
const context: FormItemContext = {
  ...props,
  validate
}

provide('form-item', context)

input 严格来说并不属于form组件,但是input也需要做校验,当值改变的时候,需要通知他父级上的el-form-item做validate校验。 在input组件中触发校验,监控值的变化,触发blur事件

const formItem = inject(formItemContextKey)
watch(
    () => props.modelValue,
    () => {
    formItem?.validate('change')
    }
)
const handleBlur = (event: FocusEvent) => {
    emit('blur', event)
    formItem?.validate?.('blur')
}

测试: image.png

重点

  • 更多组件通讯方式get!以后别说只会父子通讯,插槽传结构,ref获取组件实例等,还有祖孙级别(provide/inject),属性透传和事件监听($attrs/$listeners),获取父子组件实例($parent/$children)

  • 校验四要素的原理实现!如何获取对应字段的数据及规则,prop的作用

  • async-validator的使用