仿 ELEMENTUI 实现一个简单的 Form 表单
一、目标
ElementUI 中 Form 组件主要有以下 功能 / 模块:
- Form
- FormItem
- Input
- 表单验证
在这套组件中,有 3 层嵌套,这里面使用的是 slot 插槽,我们在接下来的代码中也需要运用到它。
二、组件设计
- e-form 全局校验,并提供插槽;
- e-form 单一项校验和显示错误信息,并提供插槽;
- e-input 负责数据的双向绑定
三、开始
e-input
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <template> <div> <!-- 1.绑定 value 2.响应 input 事件 --> <input type= "text" :value= "valueInInput" @input= "handleInput" > </div> </template> <script> export default { name: 'EInput' , props: { value: { type: String, default : '' } }, data() { return { valueInInput: this .value // 确保数据流的单向传递 } }, methods: { handleInput(e) { this .valueInInput = e.target.value; this .$emit( 'input' , this .valueInInput); // 此处提交的事件名必须是 ‘input’ // 数据变了,定向通知 formItem 进行校验 this .dispatch( 'EFormItem' , 'validate' , this .valueInInput); }, dispatch(componentName, eventName, params) { // 查找指定 name 的组件, let parent = this .$parent || this .$root; let name = parent.$options.name while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { // 这里,我们不能用 this.$emit 直接派发事件,因为在 FormItem 组件中,Input 组件的位置只是一个插槽,无法做事件监听, // 所以此时我们让 FormItem 自己派发事件,并自己监听。修改 FormItem 组件,在 created 中监听该事件。 parent.$emit.apply(parent, [eventName].concat(params)); } } } } </script> |
这里需要注意的是 v-model 绑定的值与 props 传递的值的关系,从而能将修改后的值暴露至顶层自定义组件。使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <template> <div id= "app" > <e-input v-model= "initValue" ></e-input> <div>{{ initValue }}</div> </div> </template> <script> import EInput from './components/Input.vue' ; export default { name: "app" , components: { EInput }, data() { return { initValue: '223' , }; }, }; </script> |
FormItem 的设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | <template> <div> <label v- if = "label" >{{ label }}</label> <div> <!-- 拓展槽 --> <slot></slot> <!-- 验证提示信息 --> <p v- if = "validateState === 'error'" class = "error" >{{ validateMessage }}</p> </div> </div> </template> <script> import AsyncValidator from 'async-validator' ; export default { name: 'EFormItem' , props: { label: { type: String, default : '' }, // 表单项的名称 prop: { type: String, default : '' } // 表单项的自定义 prop }, inject: [ 'eForm' ], // provide/inject,vue 跨层级通信 data() { return { validateState: '' , validateMessage: '' } }, methods: { validate() { return new Promise(resolve => { const descriptor = {}; // async-validator 建议用法; descriptor[ this .prop] = this .eForm.rules[ this .prop]; // 校验器 const validator = new AsyncValidator(descriptor); const model = {}; model[ this .prop] = this .eForm.model[ this .prop]; // 异步校验 validator.validate(model, errors => { if (errors) { this .validateState = 'error' ; this .validateMessage = errors[0].message; resolve( false ); } else { this .validateState = '' ; this .validateMessage = '' ; resolve( true ); } }) }) }, dispatch(componentName, eventName, params) { // 查找上级指定 name 的组件 var parent = this .$parent || this .$root; var name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } } }, created() { this .$on( 'validate' , this .validate); // 'validate' 事件由 e-input 组件通知,在 e-form-item 组件接收到后自行触发对应方法 }, // 因为我们需要在 Form 组件中校验所有的 FormItem , // 所以当 FormItem 挂载完成后,需要派发一个事件告诉 Form :你可以校验我了。 mounted() { // 当 FormItem 中有 prop 属性的时候才校验, // 没有的时候不校验。比如提交按钮就不需要校验。 if ( this .prop) { this .dispatch( 'EForm' , 'addFiled' , this ); } } } </script> <style scoped> .error { color: red; } </style> |
其中, methods 中的方法均是辅助方法,validate() 是异步校验的方法。
Form 的设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <template> <form> <slot></slot> </form> </template> <script> export default { name: 'EForm' , props: { model: { type: Object, required: true }, rules: { type: Object } }, provide() { // provide/inject,vue 跨层级通信 return { eForm: this // form 组件实例, 在其他组件中可以通过 this.xxx 来获取属性/方法 } }, data() { return { fileds: [] // 需要校验的 e-form-item 组件数组 } }, methods: { async validate(cb) { const eachFiledResultArray = this .fileds.map(filed => filed.validate()); const results = await Promise.all(eachFiledResultArray); let ret = true ; results.forEach(valid => { if (!valid) { ret = false ; } }); cb(ret) } }, created() { // 缓存需要检验的组件 this .fileds = []; this .$on( 'addFiled' , filed => this .fileds.push(filed)) } } </script> |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战