小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
在 Element UI 中使用表单相关组件,比如 <el-input>
、<el-select>
,可以通过在 <el-form>
和 <el-form-item>
中设置表单属性,实现子组件 size
,disabled
等属性设置,以及表单校验功能。
那么自定义的组件如何实现这些功能?
业务背景
最近产品给我提个一个需求,创建商品的时候,给商品增加标签,标签不仅需要名字,还需要颜色。于是我写了个组件。
▶ 点击查看代码
<template>
<div class="tag-input">
<div :class="['tag-list', size, 'disabled']" v-if="disabled">
<el-tag v-for="(item, index) in tagList"
:size="size"
class="tag-item"
:key="item.name"
:color="item.color">
{{ item.name }}
</el-tag>
</div>
<el-popover placement="bottom-start" @show="onShow" v-model="visible" v-else>
<div style="width: 100%">
<div style="display: flex;margin-bottom: 10px;">
<el-input v-model="newTag.name"
ref="tagInput"
style="width: 200px; margin-right: 16px;"
placeholder="填写标签名称"
size="mini"
/>
<el-color-picker v-model="newTag.color" size="mini" />
</div>
<div style="text-align: right">
<el-button size="mini" type="primary" @click="addTag">添 加</el-button>
</div>
</div>
<div :class="['tag-list', size]" slot="reference">
<el-tag v-for="(item, index) in tagList"
:size="size"
@close="onClose(index)"
class="tag-item"
:key="item.name"
:color="item.color"
closable>
{{ item.name }}
</el-tag>
</div>
</el-popover>
</div>
</template>
<script>
export default {
name: 'TagInput',
props: {
value: {
type: Array,
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String
}
},
data() {
return {
newTag: {
name: undefined,
color: undefined,
},
tagList: this.value,
visible: false,
};
},
watch: {
value(val) {
if (val !== this.tagList) {
this.tagList = val;
this.newTag = {
name: undefined,
color: undefined,
};
}
}
},
methods: {
onShow() {
this.$nextTick(() => {
this.$refs.tagInput.focus();
});
},
addTag() {
if (!this.newTag.name) {
return;
}
this.tagList.push(this.newTag);
this.newTag = {
name: undefined,
color: undefined,
};
this.onChange();
this.visible = false;
},
onClose(index) {
this.tagList.splice(index, 1);
this.onChange();
},
onChange() {
this.$emit('change', this.tagList);
this.$emit('input', this.tagList);
}
}
}
</script>
<style scoped>
.tag-list {
border: 1px solid #DCDFE6;
border-radius: 4px;
padding: 3px 6px;
cursor: pointer;
min-height: 36px;
}
.tag-list.large {
min-height: 40px;
}
.tag-list.medium {
min-height: 36px;
}
.tag-list.small {
min-height: 32px;
}
.tag-list.mini {
min-height: 28px;
}
.tag-list.disabled {
background: #F5F7FA;
cursor: not-allowed;
}
.tag-item:not(:last-child) {
margin-right: 4px;
}
</style>
复制代码
展示一下组件的使用效果。
size 和 disabled 属性的透传
Vue 有个较少用到的功能,provide 和 inject
。
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
不过注意其不能替代 vuex 使用,provide
和 inject
绑定并不是可响应的。
在 ElForm 源码中可以看到其为子组件注入了 elForm
属性,传入了其本身。
export default {
// ...
name: 'ElForm',
// ...
provide() {
return {
elForm: this
};
},
}
复制代码
在 ElFormItem 用同样的方式注入了 elFormItem
属性
export default {
// ...
name: 'ElFormItem',
// ...
provide() {
return {
elFormItem: this
};
},
}
复制代码
于是在子组件可以获取这两个属性
export default {
// ...
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
}
复制代码
然后在子组件中使用这些属性,把之前 size
和 disabled
全部改为 tagInputSize
和 tagInputDisabled
就可以在子组件中引用表单统一设置的属性。
其中 this.$ELEMENT
是引入 Element 时,传入的全局配置对象。
export default {
// ...
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
tagInputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
tagInputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
}
},
}
复制代码
自定义组件的校验
在配置表单校验规则时,子组件触发指定事件(blur
或 change
)时会触发表单项的校验。
阅读源码,发现 ElFromItem
监听了下面两个事件
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
复制代码
在 onFieldBlur()
和 onFieldChange()
中又会去执行校验逻辑。所以我们需要在子组件触发 el.form.blur
或 el.form.change
事件。
我们引入 Element UI 中的 emitter
,通过其定义的 dispatch
函数触发事件。回到我们的组件,加入如下代码。
import emitter from 'element-ui/src/mixins/emitter';
export default {
// ...
mixins: [emitter],
// ...
methods: {
// ...
onChange() {
// ...
this.dispatch('ElFormItem', 'el.form.change', this.tagList);
}
}
}
复制代码
其中 dispatch(componentName, eventName, params)
会向上寻找组件名为 componentName
的组件,并触发其 eventName
事件。
除此之外,还需要加一些对应样式,在校验失败时,组件会有红色边框,我们把组件和其他组件的错误样式统一。
.el-form-item.is-error .tag-input .tag-list {
border-color: #F56C6C;
}
.el-form-item.is-error .tag-input .tag-list.disabled {
border-color: #DCDFE6;
}
复制代码
完整代码
▶ 点击查看代码
<template>
<div class="tag-input">
<div :class="['tag-list', tagInputSize, 'disabled']" v-if="tagInputDisabled">
<el-tag v-for="(item, index) in tagList"
:size="tagInputSize"
class="tag-item"
:key="item.name"
:color="item.color">
{{ item.name }}
</el-tag>
</div>
<el-popover placement="bottom-start" @show="onShow" v-model="visible" v-else>
<div style="width: 100%">
<div style="display: flex;margin-bottom: 10px;">
<el-input v-model="newTag.name"
ref="tagInput"
style="width: 200px; margin-right: 16px;"
placeholder="填写标签名称"
size="mini"
/>
<el-color-picker v-model="newTag.color" size="mini" />
</div>
<div style="text-align: right">
<el-button size="mini" type="primary" @click="addTag">添 加</el-button>
</div>
</div>
<div :class="['tag-list', tagInputSize]" slot="reference">
<el-tag v-for="(item, index) in tagList"
:size="tagInputSize"
@close="onClose(index)"
class="tag-item"
:key="item.name"
:color="item.color"
closable>
{{ item.name }}
</el-tag>
</div>
</el-popover>
</div>
</template>
<script>
import emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'TagInput',
mixins: [emitter],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
props: {
value: {
type: Array,
},
disabled: {
type: Boolean,
default: false,
},
size: {
type: String
}
},
data() {
return {
newTag: {
name: undefined,
color: undefined,
},
tagList: this.value,
visible: false,
};
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
tagInputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
tagInputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
}
},
watch: {
value(val) {
if (val !== this.tagList) {
this.tagList = val;
this.newTag = {
name: undefined,
color: undefined,
};
this.dispatch('ElFormItem', 'el.form.change', this.tagList);
}
}
},
methods: {
onShow() {
this.$nextTick(() => {
this.$refs.tagInput.focus();
});
},
addTag() {
if (!this.newTag.name) {
return;
}
this.tagList.push(this.newTag);
this.newTag = {
name: undefined,
color: undefined,
};
this.onChange();
this.visible = false;
},
onClose(index) {
this.tagList.splice(index, 1);
this.onChange();
},
onChange() {
this.$emit('change', this.tagList);
this.$emit('input', this.tagList);
this.dispatch('ElFormItem', 'el.form.change', this.tagList);
}
}
}
</script>
<style scoped>
.tag-list {
border: 1px solid #DCDFE6;
border-radius: 4px;
padding: 3px 6px;
cursor: pointer;
min-height: 36px;
}
.tag-list.large {
min-height: 40px;
}
.tag-list.medium {
min-height: 36px;
}
.tag-list.small {
min-height: 32px;
}
.tag-list.mini {
min-height: 28px;
}
.tag-list.disabled {
background: #F5F7FA;
cursor: not-allowed;
}
.tag-item:not(:last-child) {
margin-right: 4px;
}
.el-form-item.is-error .tag-input .tag-list {
border-color: #F56C6C;
}
.el-form-item.is-error .tag-input .tag-list.disabled {
border-color: #DCDFE6;
}
</style>
复制代码
测试效果
Demo代码:
▶ 点击查看代码
<template>
<div class="test-page">
<div class="options-box">
<el-form label-width="60px" size="mini">
<el-form-item label="size">
<el-select v-model="options.size">
<el-option value="large"></el-option>
<el-option value="medium"></el-option>
<el-option value="small"></el-option>
<el-option value="mini"></el-option>
</el-select>
</el-form-item>
<el-form-item label="disabled">
<el-switch v-model="options.disabled"></el-switch>
</el-form-item>
</el-form>
</div>
<el-form :model="formData"
ref="formRef"
label-width="80px"
:size="options.size"
:disabled="options.disabled"
>
<el-form-item label="商品名称" prop="name" :rules="{
required: true, message: '请填写商品名称', trigger: 'blur'
}">
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="商品标签" prop="tags" :rules="{
required: true, type: 'array', message: '请选择商品标签', trigger: 'change'
}">
<TagInput v-model="formData.tags" @change="onChange"></TagInput>
</el-form-item>
<el-form-item>
<el-button @click="reset">重 置</el-button>
<el-button @click="submit" type="primary">提 交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import TagInput from './TagInput';
export default {
components: {
TagInput
},
data() {
return {
formData: {
name: '',
tags: []
},
options: {
size: 'medium',
disabled: false,
}
};
},
methods: {
reset() {
this.$refs.formRef.resetFields();
},
submit() {
this.$refs.formRef.validate().then(() => {
console.log(this.formData);
this.$message.success('创建成功~');
});
},
onChange(v) {
console.log(v)
}
}
}
</script>
<style scoped>
.test-page {
background: #fff;
padding: 20px;
border: 1px solid #d7d7d7;
border-radius: 5px;
width: 400px;
}
.options-box {
border: 1px solid #e9e9e9;
margin-bottom: 24px;
padding: 16px;
background: #f6f6f6;
}
</style>
复制代码