最近做了一个基于elementUI的级联选择器组件的省市区单选选择器(多选更改绑定值的读取逻辑即可),踩了几个坑,特此记录一番。坑如下:
- 问题一:静态数据使用了动态加载lazyLoad之后,保存的数据无法回显
- 问题二:当checkStrictly:true时,由于是动态加载数据,而panel中选中按钮点击后,下一级无法加载出数据
针对这两个问题,搜到的处理方式大同小异,网上的方法基本都是处理服务端数据动态加载回显问题,无法解决静态数据动态加载的回显问题,若有小伙伴遇到了跟我一样的坑,本文或许能帮助你跳出坑。
对于Cascader级联组件的API若有疑问,请查阅elementUI官方文档,本篇文章不做阐述。
先来看看第一个问题,数据无法回显,其原因在于数据是动态加载的,而组件初始化时只会加载第一层级的数据,从而无法匹配到已选中的数据,也就无法回显选中的内容。
而第二个问题,是因为checkStrictly:true时,选中按钮被选中时,不会触发lazyLoad去动态加载下一级,但会触发下一级panel展开事件,因此展开下一级后没有加载任何数据。
理清了这两个问题的原因之后,就来解决这两个问题:
问题一:解决这个问题,需要在初始化时,将已选中的数据(服务端返回的选项值)对应的选项存放到组件的选项中,绑定的值才会匹配到选项数据。
问题二:在选中按钮被选中时,会出发change事件,在change事件中手动通知Cascader中的panel组件的lazyLoad方法调用,即手动加载下一级数据。
直接奉上省市区组件的全部代码,请花10分钟耐心看完,关键方法和逻辑都已注释:
<template>
<el-cascader
v-model="value"
:options="options"
:props="{
multiple,
checkStrictly,
lazy: true,
lazyLoad
}"
clearable
:disabled="readonly"
:placeholder="placeholder"
@change="onChange"
style="width: 100%"
size="small"
ref="region"
></el-cascader>
</template>
<script>
import { isEqual } from 'lodash'
export default {
name: 'AreaPicker',
props: {
// 只读、禁用
readonly: {
type: Boolean
},
// 多选
multiple: {
type: Boolean,
default: false
},
// 省市区格式
format: {
type: String,
default: 'region' // city省市 region省市区
},
// 默认值
defaultValue: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '请选择地区'
},
// 是否父子节点不相互关联
checkStrictly: {
type: Boolean,
default: false
}
},
data () {
return {
value: [],
defaultVal: [], // 用于存放初始化时的默认值
levelCount: { // 级联可选层级,如果是省市,可选0-1-2, 如果是省市区 可选0-1-2-3
province: 1,
city: 2,
region: 3
},
options: []
};
},
watch: {
format () {
this.$nextTick(() => {
this.value = []
this.options = []
this.onChange([])
})
},
defaultValue: {
immediate: true,
handler (value) {
if (value.length > 0) {
const valueList = value.map(item => +item) || []
// 如果绑定的地区数据和传进来的数据相同,不进行默认值设置逻辑
if (isEqual(valueList, this.value)) return;
this.$nextTick(() => {
this.setDefaultOptions(valueList)
this.value = [...valueList]
this.defaultVal = [...valueList]
// console.log("默认值----》", this.value)
});
} else {
this.value = []
this.defaultVal = []
}
}
}
},
methods: {
// 设置默认值回显的options
// 该方法是处理回显问题的关键方法
setDefaultOptions (defaultValue) {
// 如果默认值只有第一级,无需查找
if (defaultValue.length <= 1) return
// 找出第一层的默认值匹配的数据
const firstLevelNodeIndex = this.options.findIndex(item => item.value === defaultValue[0])
if (firstLevelNodeIndex > -1) {
const firstLevelNode = this.options[firstLevelNodeIndex]
firstLevelNode.children = []
// 从第二级开始遍历,找出每一层级匹配的数据放在children中
// 之所以从第二级开始遍历,是因为在lazyLoad中,已经把第一级的数据全部加载完成,无需重复遍历
defaultValue.slice(1).reduce((parent, id, index) => {
// 当前应该查找的层级
const level = index + 1
// 根据当前层级的路径找出children数据
const childList = this.findChildrenByPath(defaultValue.slice(0, level))
// 过滤出匹配当前遍历id的地区
const child = childList.map(item => ({
value: item.id,
label: item.name,
leaf: level >= this.levelCount[this.format],
children: []
}))
parent.children = child
// 找出当前匹配id的那个地区
const currentArea = parent.children.find(item => item.value === id)
return currentArea
}, firstLevelNode)
// 将默认值对应的选项路径都替换,达到回显的目的
this.options[firstLevelNodeIndex] = firstLevelNode
}
},
// 数据量特别大,采用子节点懒加载方式
lazyLoad (node, resolve) {
let { level, path } = node
let nodes = []
// 获取当前点击的节点的子节点
// 如果当前层级为0,表示是最外层,即应渲染所有的一级地区数据
if (level === 0) {
// areas是cdn引入的省市区数据,在全局作用域下,也可import导入到当前组件使用
const firstLevelNodes = areas.map(item => ({
value: item.id,
label: item.name,
leaf: level >= this.levelCount[this.format] ||
!(item.children &&
item.children.length > 0), // 层级不超过指定层级,或当没有children时,都不能再展开下一级panel
children: []
}))
nodes = firstLevelNodes
this.options = firstLevelNodes
} else {
// 如过层级不是最外层,则查找其children数据
// 根据path路径去查找id,找出当前应该加载的children
const child = this.findChildrenByPath(path)
nodes = child
.filter(item => !this.defaultVal.includes(item.id)) // 过滤掉属于默认值内的地区,避免重复出现选项
.map(item => ({
value: item.id,
label: item.name,
leaf: level >= this.levelCount[this.format] ||
!(item.children &&
item.children.length > 0)
}))
}
resolve(nodes)
},
// 根据路径去查找children数据
findChildrenByPath (path) {
let data = areas
let children = []
// 遍历path,找出id匹配的地区
// 并将找出的地区的children作为下一次遍历的area源数据
path.forEach(id => {
const area = data.find(item => item.id === id)
if (area) {
data = area.children || []
children = data
}
})
return children
},
onChange (value) {
const panelRefs = this.$refs.region.$refs.panel
// 获取选中的节点
const checkedNodes = panelRefs.getCheckedNodes()[0]
// 由于开启来了懒加载,且父子节点不关联
// 当选中radio时,下一级不会自动加载数据
// 手动调用源码的lazyLoad触发加载下一级
panelRefs.lazyLoad(checkedNodes)
let res = value && value.length > 0
? {
value: value || [],
label: checkedNodes ? checkedNodes.pathLabels : []
}
: null
this.$emit('change', res)
}
}
};
</script>
<style></style>
问题解答:
为什么回显的setDefaultOptions只找出绑定的值的路径上的数据?
答:为了减小数据加载的计算量,减轻浏览器渲染压力
优化思考:
在处理问题二的时候,我灵光一闪,既然我能手动通知panel调用lazyLoad加载下一级数据,那么是不是不用通过setDefaultOptions方法去找出并设置默认值路径的选项,而是直接遍历默认值,根据当前遍历的id找出节点的children数据,再调用panel的lazyLoad方法加载下一级数据即可。有兴趣的小伙伴可以尝试尝试~