解决二次封装el-form-item与el-form中的rules联动问题

281 阅读2分钟

在项目开发中碰到下面的场景

image.png

在el-form-item的label右侧有一个图标,鼠标移入有提示效果,考虑到表单的每一项都是相同结构且表单字段较多,于是决定对el-form-item二次封装,二次封装需要在满足自定义需求的同时保证el-form-item原有的功能正常使用,最关键的是能与el-form中的配置联动,如在el-form中配置的rules,label-wodth属性。

1、在label右侧加入图标和弹窗

直接参看官方文档通过插槽实现

 <el-form-item >
    <template #label>
      <div>
        <span>{{ label }}</span>
        <el-tooltip placement="top">
          <template #content>
            <div class="tip-content" v-html="tip"></div>
          </template>
          <el-icon class="label-icon" color="#eee949" size="16"><WarningFilled /></el-icon>
        </el-tooltip>
      </div>
    </template>
    <slot></slot>
  </el-form-item>

2、解决与el-form中的rules联动的问题

要实现联动的功能,可以参考源码中的实现方式,通过查看elment plus源码可以发现下面代码片段

const formContext = inject(formContextKey, undefined)
...
const formRules = formContext?.rules
  if (formRules && props.prop) {
    const _rules = getProp<Arrayable<FormItemRule> | undefined>(
      formRules,
      props.prop
    ).value
    if (_rules) {
      rules.push(...ensureArray(_rules))
    }
  }

el-form通过provide 加 inject的方式向子组件传值,formContext为当前form的上下文环境,现在思路就很清晰了,只要能拿到el-form的上下文环境,就可以自己做任何处理

import { formContextKey } from 'element-plus';

// 注入父级el-form的上下文
const elForm = inject(formContextKey, undefined);

// el-form中定义的规则
const parentRules = computed(() => {
  return elForm?.rules?.[props.prop] || [];
});

// 对规则进行合并,考虑到直接在el-form-item中设置required的情况,对rules进行合并
// 合并规则优先级:父级规则 > 组件自身规则 > required生成规则
const mergedRules = computed(() => {
  const defaultRequiredRule = props.required
    ? { required: true, message: `${props.label}不能为空`, trigger: ['blur', 'change'] }
    : null;

  return [...parentRules.value, ...(props.rules || []), ...(defaultRequiredRule ? [defaultRequiredRule] : [])];
});

完整代码

<template>
  <el-form-item :prop="prop" :rules="mergedRules" :label-width="itemLabelWidth">
    <template #label>
      <div>
        <span>{{ label }}</span>
        <el-tooltip placement="top">
          <template #content>
            <div class="tip-content" v-html="tip"></div>
          </template>
          <el-icon class="label-icon" color="#eee949" size="16"><WarningFilled /></el-icon>
        </el-tooltip>
      </div>
    </template>
    <slot></slot>
  </el-form-item>
</template>
<script setup>
import { WarningFilled } from '@element-plus/icons-vue';
import { formContextKey } from 'element-plus';

const props = defineProps({
  label: {
    type: String,
    default: '',
  },
  prop: {
    type: String,
    default: '',
  },
  rules: {
    type: Array,
    default: () => [],
  },
  tip: {
    type: String,
    default: '',
  },
  required: {
    type: Boolean,
    default: false,
  },
  labelWidth: {
    type: [String, Number],
    default: '',
  },
});

// 注入父级el-form的上下文
const elForm = inject(formContextKey, undefined);

const parentRules = computed(() => {
  return elForm?.rules?.[props.prop] || [];
});

// 合并规则优先级:父级规则 > 组件自身规则 > required生成规则
const mergedRules = computed(() => {
  const defaultRequiredRule = props.required
    ? { required: true, message: `${props.label}不能为空`, trigger: ['blur', 'change'] }
    : null;

  return [...parentRules.value, ...(props.rules || []), ...(defaultRequiredRule ? [defaultRequiredRule] : [])];
});

// label宽度优先级:父级el-form > 组件自身
const itemLabelWidth = computed(() => {
  if (elForm?.labelWidth) {
    return elForm?.labelWidth;
  }
  return props.labelWidth;
});
</script>
<style scoped lang="less">
.label-icon {
  position: relative;
  top: 3px;
}
.tip-content {
  max-width: 400px;
}
</style>

使用方式与原本的el-form-item相同,只是多了一个tip字段

 <form-item label="数据结构" prop="dataStructure" tip="提示">
      <el-select v-model="formData.dataStructure" placeholder="请选择">
        <el-option label="分析报告" value="1" />
      </el-select>
 </form-item>

注:同理,也可以通过这种方式实现自定义组件与form中的disabled属性联动