一、组件功能
- 支持分页加载下拉选项。分页加载用的是自定义指令v-loadmore
- 当输入和删除关键词时,按当前最新的关键词进行查询,并且页码回到第一页,滚动条置顶。这里要注意
el-select的remote-method没有处理clear的场景,所以我们需要自己添加@clear="remoteMethod('')"
- 编辑时支持回显。需要传入name属性,如果给name属性使用了sync修饰符,支持将label绑定给name
- 校验:原本使用change触发,但是在输入字符或者是复制粘贴进来的内容,期望的场景是清除校验
问题场景:
实际我们期望的应该是这样:
思路:使用blur事件,但是在改变关键词时手动触发校验
- 支持防抖,默认防抖时间为500ms,支持自定义。细节处理:防抖时间内,弹出框收起,防抖需要手动终止
this.remoteMethodDebounce = debounce(this.remoteMethod, this.time)
- 支持传入api,支持自定义字段别名。细节处理:如果接口返回的label和value是一样的,需要特殊处理
// 为什么要拼接下标?--当value和lable一致时,默认为选中状态,此时再次点击选项则为取消选中
if (list.length) {
const firstItem = list[0]
if (firstItem[fieldAlias.label] === firstItem[fieldAlias.value]) {
list.forEach((item, index) => {
item[fieldAlias.value] = item[fieldAlias.value] + '-' + index
})
}
}
- 支持用户创建条目,使用el-autocomplete
二、组件封装
RemoteSelect.vue
<template>
<el-autocomplete
v-if="allowCreate"
v-model="keyword"
v-loadmore:el-autocomplete-suggestion__wrap="loadMore"
:placeholder="placeholder"
:clearable="clearable"
:disabled="disabled"
:fetch-suggestions="fetchSuggestionsDebounce"
@select="({ value }) => $emit('input', value)"
@blur="handleBlur"
@clear="$emit('input', '')"
/>
<el-select
v-else
v-model="keyword"
v-loadmore:el-select-dropdown__list="loadMore"
class="remote-select"
filterable
remote
:clearable="clearable"
:placeholder="placeholder"
:loading="loading"
:disabled="disabled"
:popper-append-to-body="false"
:remote-method="remoteMethodDebounce"
@change="handleChange"
@focus="handleFocus"
@clear="remoteMethod('')"
@visible-change="handleVisibleChange"
>
<el-option v-for="item in options" :key="item[fieldAlias.value]" :label="item[fieldAlias.label]" :value="item[fieldAlias.value]" />
</el-select>
</template>
<script>
import debounce from "./debounce"
export default {
props: {
value: { type: String, default: "" },
api: { type: Function, required: true }, // 接口函数
name: { type: String, default: "" }, // 编辑时,必传此参数,用于回显
size: { type: Number, default: 10 }, // 每页条数
debounceTime: { type: Number, default: 500 }, // 防抖时间
// 字段别名,如果不传fieldAlias,需要在core中处理下字段。payerListPageApi接口出餐本来就是要处理的,那么建议在core中处理
fieldAlias: {
type: Object,
default: () => ({
keyword: "keyword",
current: "current",
size: "size",
label: "label",
value: "value",
}),
},
allowCreate: { type: Boolean, default: false }, // 是否允许用户创建
placeholder: { type: String, default: "请输入" },
clearable: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
},
data() {
return {
keyword: "", // 关键字模糊查询
options: [], // 下拉选项
loading: false,
current: 1,
total: 0,
flag: true,
}
},
watch: {
// 由于是分页加载,当编辑时,初次渲染需要将name传递过来,带上关键字查询列表。注意:只有编辑的初次渲染才需要设置name
value: {
handler(val) {
if (this.allowCreate) {
// el-autocomplete组件回显
this.keyword = val
} else {
// el-select组件回显
if (this.flag && this.name) {
this.keyword = this.name
this.flag = false
}
}
},
},
},
created() {
const { allowCreate, fetchSuggestions, remoteMethod, debounceTime } = this
if (allowCreate) {
this.fetchSuggestionsDebounce = debounce(fetchSuggestions, debounceTime)
} else {
this.remoteMethodDebounce = debounce(remoteMethod, debounceTime)
}
},
methods: {
handleBlur() {
this.fetchSuggestionsDebounce.cancel()
this.$emit("input", this.keyword.trim())
},
fetchSuggestions(queryString, cb) {
this.current = 1
this.total = 0
this.options = []
this.getOptions(cb)
},
// 选中某一项
handleChange(val) {
const { fieldAlias, options } = this
const item = options.find((n) => n[fieldAlias.value] === val) || {}
this.$emit("input", item[fieldAlias.value])
this.$emit("update:name", item[fieldAlias.label]) // 当name属性添加了sync修饰符,这句代码可以将lable绑定给name属性
this.$emit("change", item) // 当组件使用change事件时,可以获取到当前选项的所有属性
},
handleFocus() {
if (!this.options.length) {
this.getOptions()
}
},
handleVisibleChange(visible) {
if (!visible) {
this.remoteMethodDebounce.cancel() // 在下拉框隐藏时,取消防抖。问题场景:当用户输入关键字时,点击页面其他地方,此时下拉框隐藏,但是防抖还在执行,所以需要手动取消防抖
// 下拉框隐藏时触发,模拟blur事件;blur事件有bug:当选中一个项目时,继续输入关键字,此时再失焦时不会清空,所以用visible-change事件
if (this.keyword && !this.value) {
// 如果当前有关键字,但是没有选中项目,需要清空关键字
this.remoteMethod("")
}
}
},
// 加载更多
loadMore() {
if (this.total > this.options.length) {
this.current++
this.getOptions()
} else {
console.log("没有更多了")
}
},
// 当输入框内容发生变化时会触发remoteMethod,但是清空需要手动触发
remoteMethod(name) {
this.loading = true
this.keyword = name || ""
this.reset() // 当用户输入不同的字符时,意味着需要重新加载下拉,需要将当前数据重置
this.getOptions()
if (!this.keyword) {
this.loading = false
}
this.handleChange("")
},
// 重置当前页码,总数,下拉,并且将滚动条置顶
reset() {
this.current = 1
this.total = 0
this.options = []
const targetDOM = this.$el.querySelector(".remote-select .el-select-dropdown__list")
targetDOM.scrollTop = 0
this.$emit("clearValidate") // 如果需要清除校验,可以调用此方法
},
async getOptions(cb) {
try {
const { fieldAlias, keyword, size, current } = this
const params = {
[fieldAlias.keyword]: keyword.trim(),
[fieldAlias.size]: size,
[fieldAlias.current]: current,
}
const { list = [], total = 0 } = (await this.api(params)) || {}
if (current === 1) {
this.options = list
} else {
this.options.push(...list)
}
this.total = total
// 给el-autocomplete组件添加下拉数据
cb && cb(this.options) // options选项必须要提供value值
} catch (err) {
console.error(err)
} finally {
this.loading = false
}
},
},
directives: {
loadmore: {
bind(el, binding) {
// 获取element-ui定义好的scroll盒子
const SELECTWRAP_DOM = el.querySelector(`.${binding.arg}`)
function handler() {
const CONDITION = this.scrollHeight - this.scrollTop <= this.clientHeight
if (CONDITION) {
binding.value()
}
}
SELECTWRAP_DOM.addEventListener("scroll", handler)
el.__handler__ = handler
},
unbind(el, binding) {
const SELECTWRAP_DOM = el.querySelector(`.${binding.arg}`)
if (SELECTWRAP_DOM && el.__handler__) {
SELECTWRAP_DOM.removeEventListener("scroll", el.__handler__)
delete el.__handler__
}
},
},
},
}
</script>
<style scoped>
.el-autocomplete {
width: 100%;
}
.remote-select {
width: 100%;
}
.remote-select /deep/ .el-select-dropdown__list {
height: 300px;
overflow-y: auto;
overflow-x: hidden;
}
.remote-select /deep/ .el-select-dropdown__list li {
max-width: 470px;
}
</style>
这是以前的,不够完美,留着作为参考
<template>
<el-select
v-model.trim="keyword"
v-loadmore:el-select-dropdown__list="loadMore"
class="remote-select"
:popper-append-to-body="false"
filterable
remote
reserve-keyword
clearable
placeholder="请输入关键字进行查询"
:remote-method="remoteMethod"
:loading="loading"
@change="handleChange"
@clear="remoteMethod('')"
>
<el-option
v-for="item in categoryDs"
:key="item.id"
:label="item.sageCategoryName"
:value="item.sageCategoryId"
/>
</el-select>
</template>
<script>
/*
主体逻辑:
1、第一次点击输入框,加载全部下拉,滚动到底部加载下一页
2、输入框输入关键字时,按关键字查询,滚动加载下一页
3、关键字进行补充和减少时,按当前最新的关键字进行查询,并且页码回到第一页,滚动加载下一页
4、点击输入框右侧的清空按钮时,需要手动触发remoteMethod,此时的name是''
5、编辑时的回显,需要父组件将name传过来,使用name作为关键字进行查询
*/
import { productCategoryDsApi } from '@/api/product/catesManager'
export default {
props: {
isEdit: { type: Boolean, default: false }, // 是否是编辑,编辑时需要回显
value: { type: String, default: '' },
sageCategoryName: { type: String, default: '' }, // 编辑时,必传此参数
},
data() {
return {
keyword: '', // 关键字模糊查询
categoryDs: [], // 下拉选项
loading: false,
pageLe: 20,
pageNo: 1,
total: 0,
flag: true,
}
},
watch: {
// 由于是分页加载,当编辑时,初次渲染需要将name传递过来,带上关键字查询列表。注意:只有编辑的初次渲染才需要设置categoryName
value: {
handler(val) {
if (this.flag && this.isEdit) {
this.keyword = this.sageCategoryName
this.getOptions()
this.flag = false
}
},
},
},
created() {
if (!this.isEdit) {
this.getOptions()
}
},
methods: {
// 选中某一项
handleChange(val) {
const item = this.categoryDs.find((n) => n.sageCategoryId === val) || {}
this.$emit('input', item.sageCategoryId)
this.$emit('update:sageCategoryName', item.sageCategoryName)
this.$emit('change', item)
},
// 加载更多
loadMore() {
if (this.total > this.categoryDs.length) {
this.pageNo++
this.getOptions()
}
},
// 当输入框内容发生变化时会触发remoteMethod,但是清空需要手动触发
remoteMethod(name) {
this.loading = true
this.keyword = name && name.trim()
this.reset() // 当用户输入不同的字符时,意味着需要重新加载下拉,需要将当前数据重置
this.getOptions()
if (!this.keyword) {
this.loading = false
}
},
// 重置当前页码,总数,下拉,并且将滚动条置顶
reset() {
this.pageNo = 1
this.total = 0
this.categoryDs = []
const targetDOM = document.querySelector(
'.remote-select .el-select-dropdown__list'
)
targetDOM.scrollTop = 0
this.$emit('clearValidate') // 清除校验
},
async getOptions() {
try {
const { keyword, pageLe, pageNo } = this
const params = { categoryName: keyword, pageLe, pageNo }
const { code, data } = await productCategoryDsApi(params)
this.loading = false
if (code === '000000' && Array.isArray(data.data)) {
const list = data.data.map((n) => ({
...n,
sageCategoryId: n.sageCategoryId + '',
sageCategoryName: n.categoryName,
}))
if (pageNo === 1) {
this.categoryDs = list
} else {
this.categoryDs.push(...list)
}
this.total = data.total
}
} catch (err) {
console.log(err)
this.loading = false
}
},
},
}
</script>
<style lang="scss" scoped>
.remote-select {
width: 100%;
/deep/ .el-select-dropdown__list {
height: 300px;
overflow-y: auto;
overflow-x: hidden;
li {
max-width: 470px;
}
}
}
</style>
三、使用
<el-form-item label="付款方" prop="payer">
<WewooRemoteSelect
v-model="queryForm.payer"
:name="queryForm.payer"
:api="TicketManagementInteractor.payerListPageApi"
:time="1800"
allowCreate
/>
</el-form-item>
time防抖时间allowCreate支持用户创建条目
<el-form-item label="SAGE 平台类目" prop="sageCategoryId">
<RemoteSelect
v-model="form.sageCategoryId"
:name.sync="form.sageCategoryName"
:api="PlatformProductsInteractor.sageCategoryListPageApi"
:fieldAlias="{
keyword: 'categoryName',
current: 'pageNo',
size: 'pageLe',
label: 'categoryName',
value: 'sageCategoryId'
}"
@clearValidate="clearValidate"
/>
</el-form-item>
v-model用于绑定所选中的id值name用于回显时,带参查询列表,加上sync修饰符,可以快速将lable绑定给nameapi是后端提供的接口,如果接口的入参和出参字段和组件内不一致,支持使用fieldAlias自定义clearValidate事件用于在输入或删除关键词时手动清除平台类目的校验
rules: {
sageCategoryId: [
{ required: true, message: '请选择平台类目', trigger: 'blur' }, // 这里不要用change事件
],
}
clearValidate() {
this.$refs.form.clearValidate('sageCategoryId')
}
因为使用了blur,这里我需要使用watch,实时地添加校验或去掉校验
watch: {
'form.sageCategoryId': {
handler(val) {
if (val) {
this.$refs.form.clearValidate('sageCategoryId')
} else {
this.$refs.form.validateField('sageCategoryId')
}
},
},
},
因为这个校验,在父组件中多写了一个clearValidate方法和watch监听,用起来很麻烦,能不能把这个逻辑封装到组件中,大神看到了解答一下,多谢!
四、blur校验和clearValidate结合使用
场景:当选中选项时,不应该出现提示信息
原因:使用了change事件
解决:使用blur事件,并结合clearValidate
效果:
如果你觉得这篇文章对你有用,可以看看作者封装的库xtt-utils,里面封装了非常实用的js方法。如果你也是vue开发者,那更好了,除了常用的api,还有大量的基于element-ui组件库二次封装的使用方法和自定义指令等,帮你提升开发效率。不定期更新,欢迎交流~