vue-treeselect 爬坑之路—拓展功能


vue-treeselect 爬坑之路

去年做了一個項目需要用到下拉樹,功能還需要非常強大,由於項目用的框架是Vue,Element UI,網上找了一圈,發現vue-treeselect 這個組件十分強大,比較符合自己的需求,因此果斷選擇了這個組件,沒想到光是封裝這個組件斷斷續續一共整了3個月(因為最開始選型的是自己實現,后來由於回顯問題不好解決,只好重頭開始做了),做到后面都快麻木了。現在項目結束了,現在就把自己遇到的一些坑給大家分享一下,希望有心人可以少走彎路,也歡迎批評指正。
事先聲明一下,下面單獨舉的例子應該都無法直接運行,每個例子都只是把關鍵代碼截取出來,方便大家理解。在文章的最后我會把完整的代碼貼出來,那個肯定能運行(前提是要自行安裝好包)

碰到的一些坑

坑1:回顯時出現undefined

效果如下圖所示
微信圖片_20210129150849.png

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:下拉樹寬度過小或者下拉選項層級過深時,無法看到全部的下拉選項

效果圖如下
微信圖片_20210201172643.png
原因:下拉框的寬度和上面的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; }

實現效果如下圖所示:
1.png
目前上面這種實現還有許多不如意的地方,比如只能解決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類型結果如下:
    微信圖片_20210201232005.png

需求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就是直接輸入的外部值,在失焦后就會顯示在輸入框內
微信圖片_20210201230208.png

需求4 選擇內容過多,超出限定個數無法看到所有選項值

截圖如下:目前限定最多展示三項,超出三項顯示... ,所以需求就是需要針對所有選值可以支持tooltip展示
微信圖片_20210201232606.png
分析思路:考慮到有兩種場景,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"); } }, } } 

效果圖如下:
image.png

該組件所有代碼如下(當然,還有很多內置需求沒體現在該文中)

<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
下拉樹.png

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM