极简系列---vue3.x表单组件form

本文从零开始实现一个自定义的vue3.x表单组件ti-form,组件使用体验类似element-ui。

完整代码地址:https://github.com/littleluckly/vue3.x-components-study

实现过程涉及到的知识点

  1. setup函数,用法参考:https://v3.cn.vuejs.org/guide/composition-api-setup.html
  2. toRefs
  3. ref
  4. reactive
  5. v-model
  6. 事件订阅派发,采用第三方库mitt,用法参考https://www.npmjs.com/package/mitt
  7. 表单校验,第三方库async-validator,用法参考:https://www.npmjs.com/package/async-validator
  8. provide/inject,父子/子孙数据传递,用法参考:https://v3.cn.vuejs.org/guide/composition-api-provide-inject.html#provide-inject
  9. 组件注册

需求拆解

  • 实现组件ti-form,处理表单整体校验(收集所有ti-form-item的validate)、表单data维护,表单rules校验规则维护
  • 实现组件ti-form-item,处理单个表单项组件的校验,显示表单label, 校验错误信息
  • 实现组件ti-input用于测试表单组件

ti-form组件基本结构

新建ti-form.vue,实现拆解需求提供的功能

  1. 接受model,保存表单数据
  2. 接受校验规则
  3. 提供表单整体校验方法validate,调用子组件ti-form-item的校验方法

先上一段伪代码,展示组件基本结构

<template>
  <form>
    <slot></slot>
  </form>
</template>
<script>
import { provide, reactive, toRefs } from "vue";
import mitt from "mitt";
export default {
  props: {
    model: {
      type: Object,
      default: () => ({}),
    },
    rules: {
      type: Object,
      default: () => ({}),
    },
  },
  setup(props) {
    const fields = reactive([]);
    const emitter = mitt();

    const validate = () => {
      // TODO: 调用子组件ti-form-item的校验方法
    };

    emitter.on("ti.form.addField", (field) => {
      field && fields.push(field);
    });

    return { validate };
  },
};
</script>


ti-form-item组件基本结构

  1. 接受label,用于显示表单项文本
  2. 接受prop,当前表单项的key,用于获取校验规则、表单项的值。
  3. 提供validate方式,校验当前表单项
  4. 注册自定事件validate,表单项的具体控件如ti-inputblur或者change时调用该方法进行校验
<template>
  <div class="ti-form-item">
    <label for="">
      {{ label }}
    </label>
    <slot></slot>
    <p class="errors">
      {{ error }}
    </p>
  </div>
</template>
<script>
import Schema from "async-validator";
import mitt from "mitt";
import { reactive, onMounted, ref, toRefs, provide, inject } from "vue";
export default {
  props: {
    label: {
      type: String,
    },
    prop: {
      type: String,
    },
  },
  setup(props) {
    const emitter = mitt();
    let error = ref();

    const validate = () => {
      // TODO:获取当前表单项的值进行校验
      // ?如何拿到表单项的值的呢
    };
    
    return { error, validate };
  },
};
</script>
<style scoped>
.errors {
  color: red;
  font-size: 12px;
}
</style>

ti-form-item组件校验方法

校验疑问:校验的过程其实就是规则和表单项的值进行匹配,但是ti-form-item组件又没有保存表单项的值,该怎么办呢?回想下在使用ElementUI的时候,我们并没有显示传递表单项的值,她是怎样做到呢,其实是通过provide/inject实现的。

ti-form中定义一个响应式的表单对象,将props、事件总线通过provide传递给子孙后代组件ti-form-item,如ti-inputti-select等具体的UI控件)

    // ti-form.vue文件
	const tiForm = reactive({
      formEmitter: emitter,
      ...toRefs(props),
    });
    provide("tiForm", tiForm);

在子孙后代组件ti-form-item中通过inject接受

  • 接受父组件provide提供的formEmittermodelrules
  • 将自身的formItemEmitterproprulesvalidate属性和方法,provide给子孙组件(ti-inputti-select等UI控件)
// ti-form-item.vue文件
<template>
  <div class="ti-form-item">
    <label for="">
      {{ label }}
    </label>
    <slot></slot>
    <p class="errors">
      {{ error }}
    </p>
  </div>
</template>
<script>
import Schema from "async-validator";
import mitt from "mitt";
import { reactive, onMounted, ref, toRefs, provide, inject } from "vue";
export default {
  setup(props) {
    const emitter = mitt();
    let error = ref();
    
    // 接受父组件传递的`formItemEmitter`、`prop`、`model`、`rules`属性和方法
    const tiForm = inject("tiForm");

    const validate = () => {
      // 当前表单项校验
      // 获取校验规则和当前数据
      if (!props.prop) return;
      const rules = tiForm.rules[props.prop];
      const value = tiForm.model[props.prop];
      const validator = new Schema({ [props.prop]: rules });
      // 返回promise,全局可以统一处理
      return validator.validate({ [props.prop]: value }, (errors) => {
        // errors存在则校验失败
        if (errors) {
          error.value = errors[0].message;
        } else {
          // 校验通过
          error.value = "";
        }
      });
    };
    
    // 定义响应式的表单项对象,将props、校验方法、事件总线通过`provide`传递给子孙后代组件,如`ti-input`、`ti-select`等具体的UI控件
    const tiFormItem = reactive({
      ...toRefs(props),
      formItemEmitter: emitter,
      validate,
    });
    provide("tiFormItem", tiFormItem);

    return { error, validate };
  },
};
</script>

ti-form组件校验方法

表单项组件ti-form-item已基本实现校验,继续把目光放到ti-form组件,它也需要一个校验方法,用来在表单提交前校验所有的表单项ti-form-item,实现思路是:

  • 接受一个回调函数

  • 收集所有的子组件ti-form-itemvalidate并全部触发

  • 将校验结果作为参数,传递给回调函数执行

// ti-form.vue文件
    const validate = (cb) => {
      const tasks = fields.map((item) => item.validate());
      Promise.all(tasks)
        .then(() => cb(true))
        .catch(() => {
          console.log("catch-false");
          cb(false);
        });
    };

表单校验方法,用来在表单提交前校验,其实就是调用子组件ti-form-itemvalidate方法。其主要实现步骤:

  • 父组件ti-form中通过事件总线注册一个方法收集所有子组件ti-form-itemvalidate方法
  • provide/inject将收集方法传递给子组件
  • 子组件加载完成后,调用收集方法,将自身validate方法保存到父组件中

父组件ti-form

    const fields = []
		emitter.on("ti.form.addField", (field) => {
      field && fields.push(field);
    });

    const validate = (cb) => {
      const tasks = fields.map((item) => item.validate());
      Promise.all(tasks)
        .then(() => cb(true))
        .catch(() => {
          console.log("catch-false");
          cb(false);
        });
    };

子组件ti-form-item

    onMounted(() => {
      // 注册validate事件, 用于UI控件触发校验, 如ti-input控件
      emitter.on("validate", validate);

      // 通过父组件的事件总线,将表单项校验方法传递给父组件
      if (props.prop) {
        tiForm.formEmitter.emit("ti.form.addField", tiFormItem);
      }
    });

ti-input组件

与vue2.x相比v-model发生了一点点变化

  • value改成了modelValue
  • input事件改成了update:modelValue, 类似v-bind:xxx.sync

input组件功能较为简单,主要是两个功能点

  • 实现v-model
  • blur和input事件触发校验
  • $attrs普通属性的传递
<template>
  <input type="text" :value="modelValue" @input="handleChange" @blur="handleBlur"/>
</template>
<script>
import { inject } from "vue";
export default {
  name: "ti-input",
  props: {
    modelValue: {
      type: String,
    },
  },
  setup(props, { emit }) {
    const tiFormItem = inject("tiFormItem");

    const handleChange = (e) => {
      emit("update:modelValue", e.target.value);
      tiFormItem && tiFormItem.formItemEmitter.emit("validate");
    };

    const handleBlur = () => {
      tiFormItem && tiFormItem.formItemEmitter.emit("validate");
    };
    return { handleChange, handleBlur };
  },
};
</script>


测试效果:

在这里插入图片描述

注册Emitter插件,实现全局事件触发与监听

ti-form组件和ti-form-item组件我们都引入了mitt库,注册了自定义事件,并且都provide传递给了组件,这个过程我们可以进一步优化,实现一个emitter插件,在app.config.globalPerperties中进行声明,绑定到全局属性中。如此操作后在所有组件实例中就可以通过proxy.$sub注册事件,proxy.$pub触发事件。

新建/plugins/emitter.js

import mitt from "mitt";

export default {
  install(app) {
    const _emitter = mitt();

    // 全局发布(在Vue全局方法中自定义$pub发布方法)
    app.config.globalProperties.$pub = (...args) => {
      _emitter.emit(args[0], args.slice(1));
    };

    // 全局订阅(在Vue全局方法中自定义$sub订阅方法)
    app.config.globalProperties.$sub = function(...args) {
      Reflect.apply(_emitter.on, _emitter, args);
    };

    // 取消订阅
    app.config.globalProperties.$unsub = function(...args) {
      Reflect.apply(_emitter.off, _emitter, args);
    };
  },
};

emitter注册到vue实例上

// 入口文件main.js
import { createApp } from "vue";
import App from "./App.vue";
import emitter from "./plugins/emitter";

const app = createApp(App);
app.use(emitter);
app.mount("#app");

修改ti-form中事件注册方式

    const { proxy } = getCurrentInstance();

    proxy.$sub("ti.form.addField", (field) => {
      field && fields.push(field[0]);
    });

修改ti-form-item事件触发与监听方式

    const { proxy } = getCurrentInstance();
		onMounted(() => {
      // 注册validate事件, 用于UI控件触发校验, 如ti-input控件
      proxy.$sub("ti.form.item.validate", validate);

      // 通过父组件的事件总线,将表单项校验方法传递给父组件
      if (props.prop) {
        tiForm;
        proxy.$pub("ti.form.addField", tiFormItem);
        // tiForm.formEmitter.emit("ti.form.addField", tiFormItem);
      }
    });

修改ti-input事件触发方式

    const { proxy } = getCurrentInstance();

    const handleChange = (e) => {
      emit("update:modelValue", e.target.value);
      proxy.$pub("ti.form.item.validate");
    };

全局修改完之后,效果和之前是一致的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值