本文實現級聯組件須要用到自定義指令和組件通訊相關知識,最好先閱讀如下兩篇文章:vue
Vue自定義指令json
Vue組件基礎與通訊segmentfault
本文實現的是一個省、市、縣...多級聯動組件,當組件渲染完成後默認會加載出全部的省名稱,當用戶點擊某個省的名稱後,右邊會自動添加一列顯示該省下對應的市名稱列表,當用戶點擊某個市後,右邊又會自動添加一列顯示該市下對應的縣名稱列表,同時支持級聯列表的打開和關閉。
數組
① 組件所須要的數據,數據結構很是簡單,對象裏面只有兩個屬性,一個是label(標籤名),若是當前標籤下還有子標籤,則會多一個children屬性,children屬性值爲一個數組,每一個數組元素爲其下的一個子標籤。數據結構
// data.json, 爲避免數據佔用太多篇幅,這裏只列舉了一條數據ide
[ { "label": "江西", "children": [ { "label": "贛州", "children": [ { "label": "全南縣" }, { "label": "龍南縣" } ] } ] } ]
② 咱們的級聯組件分爲上下兩部分組件,上部分顯示用戶選擇的路徑,下部分顯示用戶選擇列表,同時支持點擊級聯組件的上部分能夠實現下半部分的打開和關閉,點擊組件外面關閉組件的下半部分,這裏須要用到v-click-outside指令,這裏自定義指令的代碼就再也不重複,請參考Vue自定義指令
// Cascader.vue 新建一個Cascader.vue組件函數
<template> <div class="cascader" v-click-outside="close"> <!--實現點擊組件外面關閉組件下半部分--> <div class="title" @click="toggle">{{resultPath}}</div> <!--點擊上半部分能夠實現下部分的顯示和隱藏切換--> <div class="content" v-if="isVisible"> <!--組件下半部分,即選擇列表部分--> </div> </div> </template> <script> import clickOutside from "./../directives/clickOutside"; export default { name: "Cascader", directives: { // 在當前組件上註冊clickOutside指令 clickOutside }, props: ["options"], //定義一個options屬性用於接收外部傳遞給級聯組件的數據,即選擇項列表 data() { return { isVisible: false, selectedItems: [] // 用戶已選擇項 } }, computed: { resultPath() { // 經過用戶已選擇項計算出用戶的選擇路徑 return this.selectedItems.map((item) => item.label).join("/"); } }, methods: { close() { // 關閉下半部分(選擇列表部分) this.isVisible = false; }, toggle() { // 下半部分(選擇列表部分)顯示和隱藏的切換 this.isVisible = !this.isVisible; } } } </script>
注意到組件中有一個 selectedItems數據,這是一個 數組,默認值爲 空數組,由於當級聯組件渲染完成後,默認用戶是沒有點擊選擇其中任何一項的,只有當 用戶點擊了某一項後,纔會將 點擊的這一項添加到selectedItems數組中,其就是 記錄用戶的選擇項。這裏須要理解清楚選擇項的概念:
好比咱們的級聯組件有三列, 省、市、縣三列,結合上面的數據結構,整個省是一個大對象,即 省對象,省對象中有children屬性,裏面包括多個子對象,即 市對象,市對象中又包括children屬性,裏面包括多個子對象,即 縣對象,縣對象中再也不有children了,具體表示就是:
省對象:
{ "label": "江西", children: [省略...]}
市對象:
{ "label": "贛州", children: [省略...]}
縣對象:
{ "label": "全南縣"}
當用戶點擊 第一列,那麼就 將整個省對象添加到selectedItems數組中的 第一項位置,當用戶接着點擊了 第二列,如省對象中的label爲"贛州"的市對象,則 將整個市對象添加到selectedItems數組中的 第二項位置,當用戶又點擊了 第三列,如"贛州"市對象下的label爲"全南縣"的縣對象,則 將整個縣對象添加到selectedItems數組中的 第三項位置,這樣selectedItems數組中就保存了用戶選擇的三列數據了,而後 將三列數據中的label取出經過"/"鏈接起來,即用戶的選擇路徑"江西/贛州/全南縣"。
③ 接下來就是考慮組件拿到數據後,如何渲染的問題了?
這裏須要用到組件內遞歸組件,咱們能夠左右兩列抽象成一個單獨的組件CascaderItem.vue,可是右邊這一列會不會顯示,得看用戶有沒有選擇左邊的項,若是點擊了左邊的項則顯示右邊的列,若是沒有點擊左邊的項則不顯示右邊的列。this
仍是以省、市、縣三列爲例, 中間的市這一列,既是省的右列,也是縣的左列,咱們已經將左右兩列抽象了一個單獨的CascaderItem組件,關鍵是理解 省這一列的右邊部分究竟是什麼?,從表面上看,省這一列的右邊就是一個市列,可是若是右邊僅僅是市這一列的話,那麼 當用戶點擊市這一列中的某項的時候,就沒法顯示市右邊的縣列了,因此 省這一列的右邊其實又是一個CascaderItem組件,只有這樣點擊市列中的某一項的時候,其右邊的縣列纔會顯示出來。因此咱們須要 在CascaderItem組件內遞歸本身,而組件內遞歸本身,那麼 必須給組件添加name屬性,即 給組件取一個名字,如:
// CascaderItem.vuespa
<template> <div class="cascader-item"> <!--首先渲染出級聯組件的最左邊部分--> <div class="content-left"> <div v-for="(item, index) in options" :key="index"> <div class="label" @click="select(item)"> {{item.label}}</div> </div> </div> <!--點擊左邊中的某個選項後,lists纔會有值纔會渲染右邊部分,一樣渲染右邊部分的時候,也是先渲染左邊部分--> <div class="content-right" v-if="lists && lists.length"> <CascaderItem :options="lists" :selectedItems="selectedItems" :level="level + 1" @change="change"></CascaderItem> </div> </div> </template> <script> export default { name: "CascaderItem", // 給組件起個名字,方便組件內部遞歸調用,即組件內部本身調用本身 props: ["options", "selectedItems", "level"], computed: { lists() { // 根據內容value的變化顯示列表,根據當前點擊位置對應的level去獲取要顯示的列表 return this.selectedItems[this.level] && this.selectedItems[this.level].children; } }, } </script>
CascaderItem組件組件的渲染數據來自於頂層父組件Cascader中的selectedItems數據,由於用戶點擊了左側列中的項後, 會將點擊的item項添加到selectedItems中,selectedItems中數據變化以後纔會顯示右側的列。
CascaderItem組件須要接收一個 level屬性,用來記錄當前CascaderItem組件所屬層級,即第幾列,爲了方便,咱們 從0開始表示第一列,即第一層因此Cascader.vue中level傳入0, 後面沒加一層level會加1,如:
// 補全上面的Cascader.vue,渲染出下半部分設計
<template> <div class="cascader" v-click-outside="close"> <div class="title" @click="toggle">{{resultPath}}</div> <div class="content" v-if="isVisible"> <!--將左右兩部分封裝爲一個組件,而後循環輸出組件--> <CascaderItem :options="options" :selectedItems="selectedItems" :level="0" @change="change"></CascaderItem><!--傳入level從0開始--> </div> </div> </template>
CascaderItem組件的左邊部分都監聽了一個click事件,當用戶點擊左邊的列選項後,須要 將當前所在level和item對象數據傳遞到Cascader父組件中的selectedItems數組中,以便獲取用戶的選擇路徑,由於 單向數據流,子組件不能直接修改父組件傳遞過來的數據,因此須要 去父組件中修改數據,這裏 以事件的方式通知頂層父組件本身更新數據。
// CascaderItem.vue給CascaderItem組件添加一個select()方法
export default { methods: { select(item) { // 處理CascaderItem組件內左側列點擊事件,item爲當前點擊的對象 // 向上一級發射一個change事件,通知上層進行修改,並將當前點擊的層級level和item傳遞過去 this.$emit("change", {level: this.level, item: item}); } } }
因爲CascaderItem是遞歸調用的,因此如今的組件調用關係爲: Cascader --> CascaderItem --> CascaderItem --> CascaderItem --> ......
頂層父組件爲Cascader,因此 CascaderItem也多是CascaderItem的父組件, CascaderItem組件自身也須要監聽change事件,主要就是負責將數據改變信號傳遞到Cascader頂層父組件上,如:
// CascaderItem.vue給CascaderItem組件添加一個change事件處理方法
export default { methods: { change(newValue) { // 向頂層傳遞數據改變信息 this.$emit("change", newValue); } } }
頂層父組件Cascader接收到數據改變信號後,就須要改變selectedItems數據了,即將用戶的選擇項添加到對應的位置,如:
// Cascader.vue 添加change事件處理函數
export default { methods: { change(newValue) { this.selectedItems.splice(newValue.level, 1, newValue.item); // 替換當前點擊位置信息 this.selectedItems.splice(newValue.level + 1); // 刪除當前點擊位置以後的數據 } } }
Cascader組件除了替換掉指定level中的數據外, 還須要將當前level以後的數據刪除掉,不然當前level以後的數據還在,致使右側路徑仍然保留而顯示不一致。
至此,一個簡單的級聯組件就實現了,能夠在App.vue中直接使用,如:
// App.vue
<template> <div> <Cascader :options="options"></Cascader> <!--直接將數據傳遞給級聯組件便可--> </div> </template> <script> import Cascader from "./components/Cascader"; import dataList from "./data/data.json"; export default { components: { Cascader }, data() { return { options: dataList } } } </script>
整個Cascader組件設計思路就是: 在頂層父組件Cascader中 添加一個selectedItems數組,用於保存用戶點擊的 level層級(列序號)和對應的 item對象,同時用於 生成用戶的選擇路徑,當用戶點擊了CascaderItem組件的左側列中某項後, 經過層層傳遞事件的方式通知頂層父組件Cascader對其數據進行更新,頂層父組件Cascader更新數據後,CascaderItem組件 從selectedItems中取出對應level的item對象,而後獲取item的children並遍歷顯示右側列