vue-treeselect 爬坑之路—拓展功能
作者:互联网
vue-treeselect 爬坑之路
去年做了一个项目需要用到下拉树,功能还需要非常强大,由于项目用的框架是Vue,Element UI,网上找了一圈,发现vue-treeselect 这个组件十分强大,比较符合自己的需求,因此果断选择了这个组件,没想到光是封装这个组件断断续续一共整了3个月(因为最开始选型的是自己实现,后来由于回显问题不好解决,只好重头开始做了),做到后面都快麻木了。现在项目结束了,现在就把自己遇到的一些坑给大家分享一下,希望有心人可以少走弯路,也欢迎批评指正。
事先声明一下,下面单独举的例子应该都无法直接运行,每个例子都只是把关键代码截取出来,方便大家理解。在文章的最后我会把完整的代码贴出来,那个肯定能运行(前提是要自行安装好包)
碰到的一些坑
坑1:回显时出现undefined
效果如下图所示
treeselect 绑定的值需要与options输出的id相对应,若是空值,必须是null,请不要给"",0,等,因为会出现unknown,并且当选择了值以后,会出现选中的值后面会拼上unknown
问题的原因如上所述,但是我们常见的需求是需要回显下拉树的值,此时值已经绑定,但是树选项还在加载中,那么就会出现短暂的unknown,稍后就会恢复正常。我的解决思路是判断是否还在加载下拉树的选项,如果加载完就显示实际值,否则赋值null。实际落地就是要结合computed的get函数来实现。
实际关键代码如下:
<template> <treeselect v-model="treeValue" :options="getOptions" > </treeselect> </template>
export default { data: () => ({ options:[],//下拉树选项 }), computed:{ getOptions(){ return this.options; }, treeValue:{ set(val){ // this.$emit('change',val); }, get(){ //没有数据时不显示 if( this.options.length == 0 ){ return null; } return this.value; } } }, }
好了,刷新页面,发现效果达到预期,详细完整代码见最下方
坑2:下拉树宽度过小或者下拉选项层级过深时,无法看到全部的下拉选项
效果图如下
原因:下拉框的宽度和上面的input框的宽度保持一致,只要下面的内容过深过长时,就会出现遮挡,此时需要支持手动拖拽下拉框的边框,从而改变下拉款宽度。我的具体实现思路是是在打开下拉选项时添加鼠标事件监听。
关键代码如下:
<template> <treeselect :multiple="multiple" v-model="treeValue" :options="getOptions" :normalizer="normalizer" :appendToBody="appendToBody" :limit="3" :limitText="count => `...`" :maxHeight="200" :placeholder="placeholder" flat @open="open" > </treeselect> </template>
export default { props:{ //是否挂靠在body上,一般不需要,特殊情况是表格新增行时需要注意加上 appendToBody:{ type:Boolean, default:true, }, }, data: () => ({ }), methods: { //增加拖拽下拉功能 open(instanceId){ let dom = document.querySelector(`.vue-treeselect[data-instance-id='${instanceId}']`); let listDom; this.$nextTick(()=>{ //只有在挂靠在appendToBody下实现拖拽功能 if(!this.appendToBody) return; // listDom = dom.querySelector(".vue-treeselect__menu"); if(listDom) { let startX = listDom.getBoundingClientRect().right; let oldWidth = dom.getBoundingClientRect().width; //原宽度 //初始化参数 listDom.onmousedown = function(e){ // e.stopPropagation(); let curDom = e.target; //捕捉焦点 //设置事件 document.onmousemove = function (ev) { if(ev.clientX - startX>0){ dom.style.width = oldWidth+ ev.clientX - startX + "px"; } }; document.onmouseup = function (ev) { ev.stopPropagation(); document.onmousemove = null; document.onmouseup = null; }; //防止默认事件发生 e.preventDefault(); }; } }) }, }
//只在append-to-body下实现拖拽功能手势 .vue-treeselect--append-to-body .vue-treeselect__menu{ cursor: e-resize; }
实现效果如下图所示:
目前上面这种实现还有许多不如意的地方,比如只能解决appendToBody的情况,还有如果下拉选项如果出现滚动条时,鼠标浮上去无法进行拖拽,得左右偏移一点才能拖拽。没办法,由于当时项目比较紧,还有自己比较菜,所以只好先这样了,如果有大神觉得可以改进,欢迎指教。
坑3:ElementUI的table行内使用vue-treeselect,下拉框无法显示
该问题有个帖子写的比较好,我就不重复写了,当时我还没看到这篇帖子,自己用设置append-to-body属性的方式来解决的,详细地址如下:elementui组件table行内使用vue-treeselect无效
坑4 You are using flat mode. But you forgot to add "multiple=true"
出现这个原因是由于vue-treeselect不支持单选下的flat模式,但是我的需求又必须得有,由于发现该库作者已经很久没有维护代码了,在没有办法情况下我只好看了一下源码,发现这个报错知识一个提示,并不会有什么实际影响,所以我决定把源码的这个提示去掉,然后上传新的文件到npm,这样就可以避免该问题。我自己上传了这个资源到npm,需要的同学可以直接下载。这个y_treeselect和源码并没有什么区别,就是把这个提示去掉了,所以可以放心使用
$ npm i -S y_treeselect
实现的一些特殊需求
需求1:父子节点没有关联,还要实现特殊情况下只能选择叶子节点
关于前半个要求,其实用平面模式就可以搞定,后半个需求借助disableBranchNodes属性可以实现。
关键代码如下:
-
组件外部引用
<MyTreeSelect isChildOnly multiple v-model="treeValue1" />
-
组件内部引用
<treeselect v-model="treeValue" :options="getOptions" flat value-consists-of="BRANCH_PRIORITY" :disableBranchNodes="isChildOnly" > </treeselect>
export default { props:{ isChildOnly:{//是否只能选择或者点击叶子节点 type:Boolean, default:false, }, } }
需求2 支持返回值为id或者node节点
这个也简单,api可以支持,这里只是为了后面做铺垫用的
-
组件外部引用
<gl-select-tree2 :multiple="true" valueFormat="object" v-model="treeValue1"/> {{treeValue1}} ... treeValue1:[{id:"2-1-1"}],//定义在data中,此处只需对象中有id属性即可
-
组件内部定义
<treeselect v-model="treeValue" :options="getOptions" :valueFormat="valueFormat" > </treeselect>
export default { props:{ valueFormat:{//定义返回的值为id还是整个node节点数据 type:String, default:"id",//id和object两种类型 }, } }
object类型结果如下:
需求3 支持增加搜索文本作为下拉树值
目前的下拉树只支持选项中有的才能选,但是我们需要在输入框中输入下拉树外部数据,比如外部的组织机构,输入完失焦直接显示在输入框中
关键代码如下:
<treeselect ... > <div slot="value-label" slot-scope="{ node }">{{ renderTrueValue(node.label) }}</div> </treeselect>
export default { props:{ isSupportExternalInput:{//是否支持增加搜索文本作为下拉树值 type:Boolean, default:false, }, }, methods:{ close(v, instanceId){ let val = this.$el.querySelector(".vue-treeselect__input").value; if(this.isSupportExternalInput){ let newVal = val.trim();//清除空格 if(newVal === ""){ this.$el.querySelector(".vue-treeselect__input").value = ""; return; } let value; if(this.multiple){ value = this.value.slice(); if(this.valueFormat == "object"){ newVal = {id:newVal}; } value.push(newVal);//清除尾部空格 }else{//单选 value = this.valueFormat == 'object'?{id:newVal}:newVal; this.$el.querySelector(".vue-treeselect__input").blur();//收起下拉 } this.$emit("change",value); setTimeout(()=>{//清空搜索值 this.$el.querySelector(".vue-treeselect__input").value = ""; },0) } }, //针对外部输入值时将unknown换成外部 renderTrueValue(label){ if(label.includes("(unknown)")){//隐藏不匹配时的(unknown) return label.replace('(unknown)',"(外部)") } return label; }, } }
实现的结果如下:其中sdf就是直接输入的外部值,在失焦后就会显示在输入框内
需求4 选择内容过多,超出限定个数无法看到所有选项值
截图如下:目前限定最多展示三项,超出三项显示... ,所以需求就是需要针对所有选值可以支持tooltip展示
分析思路:考虑到有两种场景,1. 可能一开始就需要回显很多项,此时放在mounted里执行 2. 可以在选择的过程中选了很多项,此时可以借助input事件来实现
关键代码如下:
export default { methods:{ inputChange(val,instanceId){ this.$emit("change",val); if(this.multiple){//只有多选模式下才考虑提示功能 this.allLabel = val.map(item=>{ let label = ""; //getNode是我自己查找下拉树的内置方法,呕心沥血才找到的 label = this.$children[0].getNode(this.valueFormat == "object"?item.id:item).label; label = label.replace('(unknown)',"(外部)"); return label; }) let el = this.$el.querySelector(".vue-treeselect__multi-value"); el.setAttribute("title",this.allLabel.join(" , ")); }else{ this.removePlaceHolder(); } this.addPlaceHolder(val); }, //增加文字提示tooltip addPlaceHolder(value){ let placeholder = this.$el.querySelector(".vue-treeselect__placeholder"); let temp = value !== "_first"? value:this.value; if(placeholder && (!temp || !temp.length)){ let content = placeholder.innerText; placeholder.parentNode.setAttribute("title",content); } }, removePlaceHolder(){ let placeholder = this.$el.querySelector(".vue-treeselect__placeholder"); if(placeholder){ placeholder.parentNode.removeAttribute("title"); } }, } }
效果图如下:
该组件所有代码如下(当然,还有很多内置需求没体现在该文中)
<template> <treeselect :multiple="multiple" v-model="treeValue" :options="getOptions" :normalizer="normalizer" :appendToBody="appendToBody" :disableBranchNodes="isChildOnly" value-consists-of="BRANCH_PRIORITY" :valueFormat="valueFormat" :limit="3" :limitText="count => `...`" :maxHeight="200" :placeholder="placeholder" flat :autoLoadRootOptions="true" @open="open" @close="close" @input="inputChange" > <div slot="value-label" slot-scope="{ node }">{{ renderTrueValue(node.label) }}</div> </treeselect> </template> <script> import Treeselect from '@riophae/vue-treeselect' export default { model:{ prop:'value', event:'change', }, components: { Treeselect }, data: () => ({ options:[],//下拉树选项 normalizer(node){ return { id: node.id , label: node.text , children: node.children, } }, }), props:{ multiple:Boolean, value: {default:null}, placeholder:{default:'请选择'}, //是否挂靠在body上,一般不需要,特殊情况是表格新增行时需要注意加上 appendToBody:{ type:Boolean, default:true, }, isChildOnly:{//是否只能选择或者点击叶子节点 type:Boolean, default:false, }, isSupportExternalInput:{//是否支持增加搜索文本作为下拉树值 type:Boolean, default:false, }, valueFormat:{//定义返回的值为id还是整个node节点数据 type:String, default:"id",//id和object两种类型 }, }, created(){ }, mounted(){ this.addPlaceHolder("_first");//首次进来 this.generateOptions(); }, computed:{ getOptions(){ return this.options; }, treeValue:{ set(val){ let temp = val; if(val === "" || val === undefined){ temp = null; } this.$emit('change',temp); }, get(){ //没有数据时不显示 if( this.options.length == 0 ){ return null; } return this.value; } } }, watch:{ }, methods: { //生成初始选项 generateOptions(){ //模拟网络请求 setTimeout(()=>{ this.options = sOptions; },1000); }, inputChange(val,instanceId){ this.$emit("change",val); if(this.multiple){//只有多选模式下才考虑提示功能 this.allLabel = val.map(item=>{ let label = ""; //getNode是我自己查找下拉树的内置方法,呕心沥血才找到的 label = this.$children[0].getNode(this.valueFormat == "object"?item.id:item).label; label = label.replace('(unknown)',"(外部)"); return label; }) let el = this.$el.querySelector(".vue-treeselect__multi-value"); el.setAttribute("title",this.allLabel.join(" , ")); }else{ this.removePlaceHolder(); } this.addPlaceHolder(val); }, //增加文字提示tooltip addPlaceHolder(value){ let placeholder = this.$el.querySelector(".vue-treeselect__placeholder"); let temp = value !== "_first"? value:this.value; if(placeholder && (!temp || !temp.length)){ let content = placeholder.innerText; placeholder.parentNode.setAttribute("title",content); } }, removePlaceHolder(){ let placeholder = this.$el.querySelector(".vue-treeselect__placeholder"); if(placeholder){ placeholder.parentNode.removeAttribute("title"); } }, //增加拖拽下拉功能 open(instanceId){ let dom = document.querySelector(`.vue-treeselect[data-instance-id='${instanceId}']`); let listDom; this.$nextTick(()=>{ if(!this.appendToBody) return; // listDom = dom.querySelector(".vue-treeselect__menu"); if(listDom) { let startX = listDom.getBoundingClientRect().right; let oldWidth = dom.getBoundingClientRect().width; //原宽度 //初始化参数 listDom.onmousedown = function(e){ // e.stopPropagation(); let curDom = e.target; //捕捉焦点 //设置事件 document.onmousemove = function (ev) { if(ev.clientX - startX>0){ dom.style.width = oldWidth+ ev.clientX - startX + "px"; } }; document.onmouseup = function (ev) { ev.stopPropagation(); document.onmousemove = null; document.onmouseup = null; }; //防止默认事件发生 e.preventDefault(); }; } }) }, close(v, instanceId){ let val = this.$el.querySelector(".vue-treeselect__input").value; if(this.isSupportExternalInput){ let newVal = val.trim();//清除空格 if(newVal === ""){ this.$el.querySelector(".vue-treeselect__input").value = ""; return; } let value; if(this.multiple){ value = this.value.slice(); if(this.valueFormat == "object"){ newVal = {id:newVal}; } value.push(newVal);//清除尾部空格 }else{//单选 value = this.valueFormat == 'object'?{id:newVal}:newVal; this.$el.querySelector(".vue-treeselect__input").blur();//收起下拉 } this.$emit("change",value); setTimeout(()=>{//清空搜索值 this.$el.querySelector(".vue-treeselect__input").value = ""; },0) } }, //针对外部输入值时将unknown换成外部 renderTrueValue(label){ if(label.includes("(unknown)")){//隐藏不匹配时的(unknown) return label.replace('(unknown)',"(外部)") } return label; }, }, } const sOptions = [{ id:'1-1', hasChildren: true, text:'教育局', children:[ { id:'2-1', hasChildren: true, text:'教育处1', children:[{ id:'2-1-1', hasChildren: false, text:'老师1', }] }, { id:'2-2', hasChildren: true, text:'教育处2', children:[{ id:'2-2-2', hasChildren: false, text:'老师2', },{ id:'2-2-3', hasChildren: false, text:'老师3', },{ id:'2-2-4', hasChildren: false, text:'老师4', }] }, { id:'3-2', hasChildren: true, text:'教育处3', children:[{ id:'3-2-1', hasChildren: false, text:'老师2', },{ id:'3-2-2', hasChildren: false, text:'老师3', },{ id:'3-2-3', hasChildren: false, text:'老师4', }] }, ], }] </script>
<style lang="scss"> //只在append-to-body下实现拖拽功能 .vue-treeselect--append-to-body .vue-treeselect__menu{ cursor: e-resize; } </style>
完整的代码详见我的github代码,里面又大量的实例和有趣的组件,欢迎star!
vue-awesome-demos
标签:vue,之路,value,label,let,treeselect,id 来源: https://www.cnblogs.com/webSnow/p/16159483.html