相关代码见github
准备工作
项目创建
1
| vue create vue-element-form
|
按需引入 element-ui
1 2 3
| npm i element-ui -S # 安装 element-ui npm i babel-plugin-component -D # 准备按需引入 npm i async-validator -S # Form 表单校验
|
修改 babel.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| module.exports = { presets: [ '@vue/app', [ '@babel/preset-env', { modules: false } ] ], plugins: [ [ 'component', { libraryName: 'element-ui', styleLibraryName: 'theme-chalk' } ] ] };
|
main.js 按需引入
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
| import { Form, FormItem, Input, Select, Option, Col, DatePicker, TimePicker, Switch, CheckboxGroup, Checkbox, RadioGroup, Radio, Button } from 'element-ui';
Vue.use(Form); Vue.use(FormItem); Vue.use(Input); Vue.use(Select); Vue.use(Option); Vue.use(Col); Vue.use(DatePicker); Vue.use(TimePicker); Vue.use(Switch); Vue.use(CheckboxGroup); Vue.use(Checkbox); Vue.use(RadioGroup); Vue.use(Radio); Vue.use(Button);
|
Home.vue 添加 Element 示例代码
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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
| <template> <div class="home"> <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm" > <el-form-item label="活动名称" prop="name"> <el-input v-model="ruleForm.name"></el-input> </el-form-item> <el-form-item label="活动区域" prop="region"> <el-select v-model="ruleForm.region" placeholder="请选择活动区域"> <el-option label="区域一" value="shanghai"></el-option> <el-option label="区域二" value="beijing"></el-option> </el-select> </el-form-item> <el-form-item label="活动时间" required> <el-col :span="11"> <el-form-item prop="date1"> <el-date-picker type="date" placeholder="选择日期" v-model="ruleForm.date1" style="width: 100%;" ></el-date-picker> </el-form-item> </el-col> <el-col class="line" :span="2">-</el-col> <el-col :span="11"> <el-form-item prop="date2"> <el-time-picker placeholder="选择时间" v-model="ruleForm.date2" style="width: 100%;" ></el-time-picker> </el-form-item> </el-col> </el-form-item> <el-form-item label="即时配送" prop="delivery"> <el-switch v-model="ruleForm.delivery"></el-switch> </el-form-item> <el-form-item label="活动性质" prop="type"> <el-checkbox-group v-model="ruleForm.type"> <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox> <el-checkbox label="地推活动" name="type"></el-checkbox> <el-checkbox label="线下主题活动" name="type"></el-checkbox> <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox> </el-checkbox-group> </el-form-item> <el-form-item label="特殊资源" prop="resource"> <el-radio-group v-model="ruleForm.resource"> <el-radio label="线上品牌商赞助"></el-radio> <el-radio label="线下场地免费"></el-radio> </el-radio-group> </el-form-item> <el-form-item label="活动形式" prop="desc"> <el-input type="textarea" v-model="ruleForm.desc"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitForm('ruleForm')"> 立即创建 </el-button> <el-button @click="resetForm('ruleForm')">重置</el-button> </el-form-item> </el-form> </div> </template>
<script> export default { data() { return { ruleForm: { name: '', region: '', date1: '', date2: '', delivery: false, type: [], resource: '', desc: '' }, rules: { name: [ { required: true, message: '请输入活动名称', trigger: 'blur' }, { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' } ], region: [ { required: true, message: '请选择活动区域', trigger: 'change' } ], date1: [ { type: 'date', required: true, message: '请选择日期', trigger: 'change' } ], date2: [ { type: 'date', required: true, message: '请选择时间', trigger: 'change' } ], type: [ { type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' } ], resource: [ { required: true, message: '请选择活动资源', trigger: 'change' } ], desc: [{ required: true, message: '请填写活动形式', trigger: 'blur' }] } }; }, methods: { submitForm(formName) { this.$refs[formName].validate(valid => { if (valid) { alert('submit!'); } else { console.log('error submit!!'); return false; } }); }, resetForm(formName) { this.$refs[formName].resetFields(); } } }; </script>
<style scoped> .demo-ruleForm { margin: 0 auto; width: 50%; } </style>
|
修改路由
将 view/About.vue
改名为 Custom.vue
index.js
1 2 3 4 5 6 7 8
| import Vue from 'vue' import VueRouter from 'vue-router' import Home from '../views/Home.vue' Vue.use(VueRouter) const routes = [ { path: "/", name: "home", component: Home }, { path: "/custom", name: "custom", code-splitting route import( "../views/Custom.vue") } ]; const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <div id="app"> <div id="nav"> <router-link to="/">ElementUI Form</router-link> | <router-link to="/custom">Custom Form</router-link> </div> <router-view /> </div> </template>
...
|
组件结构
在 components
文件夹下新建 swForm
文件夹作为自定义组件:
1 2 3 4 5
| └── swForm ├── SwForm.vue ├── SwFormItem.vue ├── SwInput.vue └── index.vue
|
最终使用
index.vue
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
| <template> <div> <h3>自定义Form表单组件</h3> <hr /> <sw-form :model="model" :rules="rules" ref="loginForm"> <sw-form-item label="用户名" prop="username"> <sw-input v-model="model.username" autocomplete="off" placeholder="输入用户名" ></sw-input> </sw-form-item> <sw-form-item label="确认密码" prop="password"> <sw-input type="password" v-model="model.password" autocomplete="off" ></sw-input> </sw-form-item> <sw-form-item> <button @click="submitForm('loginForm')">提交</button> </sw-form-item> </sw-form> {{model}} </div> </template>
<script> import SwForm from './SwForm'; import SwFormItem from './SwFormItem'; import SwInput from './SwInput';
export default { components: { SwForm, SwFormItem, SwInput }, data() { return { model: { username: 'Lance', password: '' }, rules: { username: [ { required: true, message: '请输入用户名' } ], password: [ { required: true, message: '请输入密码' } ] } }; }, methods: { submitForm(form) { this.$refs[form].validate(valid => { alert(valid ? '开始请求登录!' : '校验失败'); }); } } }; </script>
|
预留插槽 slot
根据上面的表单组件结构图,可以分别给 SwForm
、SwFormItem
设置 slot 插槽:
SwForm
1 2 3 4 5
| <template> <div> <slot></slot> </div> </template>
|
SwFormItem
1 2 3 4 5
| <template> <div> <slot></slot> </div> </template>
|
知识点 - v-model 本质
Vue 中对于 v-model
的解释: v-model
本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。v-model
会忽略所有表单元素的 「value」、「checked」、「selected」 特性的初始值而总是将 Vue 实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data 选项中声明初始值。
所以 v-model
其实只是将值绑定到 value
属性上,同时当组件触发 input 时用参数覆盖 value 属性,从而实现双向绑定。
所以当我们自定义组件要实现双向绑定,只要在自定义组件中引入 value 的 props,同时当需要改变 value 时抛出 input 事件就 OK 了。
所以我们在 SwInput.vue
中的代码实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div> <input :value="value" @input="onInput" /> </div> </template>
<script> export default { props: { value: { type: String, default: '' } }, methods: { onInput(e) { this.$emit('input', e.target.value); } } }; </script>
|
如果我们观察 element-ui 提供的 el-input
组件会发现,设置的 placeholder
属性能作用在最终的真正的 input 元素上:
代码
1
| <el-input placeholder="请输入活动名称"></el-input>
|
页面展示
1 2 3 4 5 6
| <input type="text" autocomplete="off" placeholder="请输入活动名称" class="el-input__inner" />
|
然而我们的 SwInput
组件目前的 HTML 结构为:
1 2 3
| <div> <input :value="value" @input="onInput" /> </div>
|
如果我们在使用 SwInput
组件并设置 placeholder
时,最终的属性会作用在外层的 div 上,而不是 input 上:
代码
1 2 3 4 5
| <sw-input v-model="model.username" autocomplete="off" placeholder="输入用户名" ></sw-input>
|
页面效果
1 2 3 4 5 6 7 8
| <div data-v-a9396722="" autocomplete="off" placeholder="输入用户名" data-v-43522ae2="" > <input data-v-a9396722="" /> </div>
|
那怎么才能把添加到组件上的 placeholder
属性交给里边的 input 元素呢?
通常情况下,我们会使用 props 传值的方式完成,但这种方案有种缺陷,那就是一旦我们有很多类似 placeholder
这样的自定义的属性需要设置,那 props 就会非常臃肿。
所以我们使用另一种方案:Vue.js - inheritAttrs
官方文档上说:
通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例属性 $attrs 可以让这些特性生效,且可以通过 v-bind 显性的绑定到非根元素上。
一句话总结,我们可以利用 Vue 的 inheritAttrs api,让添加到组件上的属性最终作用于非根元素上。
接下来我们用这种方案改造 SwInput
:
SwInput
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <template> <div> <input :value="value" @input="onInput" v-bind="$attrs" /> </div> </template>
<script> export default { inheritAttrs: false, ... } </script>
|
这样一来我们就把属性设置给了真正的 SwInput
组件中的 input 元素。
SwFormItem
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
| <template> <div> <label v-if="label">{{ label }}</label> <slot></slot> <p v-if="errorMessage">{{ errorMessage }}</p> </div> </template>
<script> export default { props: { label: { type: String, default: '' }, prop: { type: String } }, data() { return { errorMessage: '' }; } }; </script>
|
使用 provide/inject 传递数据
由于我们的 model
和 rules
是传给了最外层的 SwForm
,而真正做校验的是 SwFormItem
,并且校验是离不开规则 rules
的,所以我们得把数据传给子组件,这里使用 provide/inject ,它的作用 Vue 官方有写:
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。
所以我们接下来需要在 SwForm
中通过 provide 将数据传给后代子孙,后代子孙(SwFormItem
)通过 inject 接收数据。
SwForm
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
| <template> <div> <slot></slot> </div> </template>
<script> export default { provide() { return { form: this }; }, props: { model: { type: Object, required: true }, rules: { type: Object } } }; </script>
|
SwFormItem
1 2 3 4 5 6 7 8
| ... <script> import Schema from 'async-validator'; export default { inject: ["form"], ... } </script>
|
我们的校验通常在 input 元素的 input 事件触发以后,所以得在 SwInput
组件中派发事件,然后在它的父组件 SwFormItem
中监听事件:
SwInput
1 2 3 4 5 6 7 8 9 10 11 12 13
| ... <script> export default { ... methods: { onInput(e) { this.$emit("input", e.target.value);
this.$parent.$emit('validate'); } }, } </script>
|
SwFormItem
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
| <script> import Schema from 'async-validator'; export default { inject: ['form'], props: { label: { type: String, default: '' }, prop: { type: String } }, data() { return { errorMessage: '' }; }, mounted() { this.$on('validate', this.validate); }, methods: { validate() { const value = this.form.model[this.prop]; const rules = this.form.rules[this.prop];
const desc = { [this.prop]: rules }; const schema = new Schema(desc); return schema.validate( { [this.prop]: value }, errors => { this.errorMessage = errors ? errors[0].message : ''; } ); } } }; </script>
|
写完上述代码后,当你将表单中的文本框内容清空,就能看到校验提示了。
当用户填完所有表单点击提交时,我们还得将整个表单需要校验的地方都校验一遍,然后告知用户是否成功通知校验,所以我们还得在 SwForm
组件中添加一个 validate
方法,用于判断所有需要校验的地方是否都通过了校验:
SwForm
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
| <template> <div> <slot></slot> </div> </template>
<script> export default { provide() { return { form: this }; }, props: { model: { type: Object, required: true }, rules: { type: Object } }, methods: { validate(cb) { const tasks = this.$children .filter(item => item.prop) .map(item => item.validate()); console.log(tasks); Promise.all(tasks) .then(() => cb(true)) .catch(() => cb(false)); } } }; </script>
|