小知識點:
- 如果你的文件是不需要編譯的最后文件名前加一個下劃線(_),比如_var.scss
2.vue的屬性不能以data開頭妆丘,否則轉成默認屬性
比如:datasource只能寫成source
3.如果你在當前組件中使用了與你name相同的標簽,那么name就是你當前的組件
4.我們不知道要渲染的數(shù)據(jù)有多少層,我們該怎么用v-for遍歷车柠?
比如有些區(qū)下面有鎮(zhèn)竹祷,有的沒有塑陵,也就是說無法確定當前數(shù)據(jù)數(shù)組有幾層的情況下令花,那么我們可以通過遞歸組件讓組件(通過組件自己調用自己來實現(xiàn))
小案例
- index.html
<div id="app" style="padding: 100px;">
<g-cascader :source="source"></g-cascader>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
source: [
{
name: '浙江',
children: [
{
name: '杭州',
children: [
{name: '上城'},
{name: '下城'},
{name: '江干'}
]
},{
name: '嘉興',
children: [
{name: '南湖'},
{name: '秀洲'},
{name: '嘉善'}
]
}
]
},
{
name: '福建',
children: [
{
name: '福州',
children: [
{name: '鼓樓'},
{name: '臺江'},
{name: '蒼山'}
]
}
]
}
]
}
})
</script>
- cascader.vue
<template>
<div class="cascader">
<div class="popover">
<div v-for="item in source">
<cascader-item :sourceItem="item"></cascader-item>
</div>
</div>
</div>
</template>
<script>
import CascaderItem from './cascaderitem.vue'
export default {
name: 'GuluCascader',
props: {
source: {
type: Array
}
},
components: {CascaderItem}
}
</script>
- cascaderitem.vue
<template>
<div class="cascader-item">
{{sourceItem.name}}
<gulu-cascader-item v-for="(item,index) in sourceItem.children"
v-if="sourceItem.children"
:sourceItem="item"
:key="index"
>
</gulu-cascader-item>
</div>
</template>
<script>
export default {
name: 'GuluCascaderItem',
props: {
sourceItem: {
type: Object
}
}
}
</script>
上面的代碼在index里使用了cascader.vue這個組件,這個組件接受父組件傳進來的source數(shù)組扮碧,
然后遍歷最開始的數(shù)組慎王,拿到數(shù)組里面第一層的每一項宏侍,之后通過遞歸組件cascaderitem.vue接受你拿到的數(shù)組第一層的每一項(對象)谅河,顯示你這每一項里面的name旧蛾,然后調用自己再次進行遍歷判斷你傳進來的sourceItem.children是否存在锨天,如果存在就繼續(xù)遍歷souceItem.children病袄,把拿到的新的對象item傳給souceItem,然后再次顯示這里面每一項的name基公,直到對應的item下面的children不存在為止轰豆。這里要注意遞歸組件自己本身要傳入的屬性酸休,在自己內部也要再寫一遍斑司,就像上面的:sourceItem="item"
以上面的案例為例但汞,一開始在cascader.vue組件里傳入的是最開始的數(shù)據(jù)私蕾,然后遍歷分別拿到的item是
{
name: '浙江',
children: [
{
name: '杭州',
children: [
{name: '上城'},
{name: '下城'},
{name: '江干'}
]
},{
name: '嘉興',
children: [
{name: '南湖'},
{name: '秀洲'},
{name: '嘉善'}
]
}
]
},
{
name: '福建',
children: [
{
name: '福州',
children: [
{name: '鼓樓'},
{name: '臺江'},
{name: '蒼山'}
]
}
]
}
上面兩個對象,之后分別調用遞歸組件把這兩個對象作為item依次傳進去懊纳,開始執(zhí)行cascaderitem.vue組件里的代碼嗤疯,以第一個浙江的為例茂缚,拿到{{sourceItem.name}}也就是浙江屋谭,然后執(zhí)行下面的代碼
<gulu-cascader-item v-for="(item,index) in sourceItem.children"
v-if="sourceItem.children"
:sourceItem="item"
:key="index"
>
</gulu-cascader-item>
也就是把當前的組件里的代碼再執(zhí)行一遍桐磁,它先判斷sourceItem.children是否存在我擂,因為sourceItem.children是
{
name: '杭州',
children: [
{name: '上城'},
{name: '下城'},
{name: '江干'}
]
}
{
name: '嘉興',
children: [
{name: '南湖'},
{name: '秀洲'},
{name: '嘉善'}
]
}
所以再次對上面的對象也就是sourceItem.children再次進行遍歷看峻,得到的item分別為上面的兩個互妓,然后把這兩個賦值給sourceItem,也就是sourceItem分別是杭州和嘉興下面的對象霉猛,然后再分別使用這個組件惜浅,如此循環(huán)坛悉,最終得到下面結構
- 屬性不要以show開頭,因為以show開頭的都是函數(shù)
頁面渲染初步實現(xiàn)
- 正常情況下在已知有幾層數(shù)據(jù)的情況下的視圖渲染這里以上面的三層為例
<template>
<div class="cascader">
<div class="popover" @click="popoverVisibility = !popoverVisibility">
</div>
<div class="leave" v-if="popoverVisibility">
<div class="leave1">
<div v-for="item in source" @click="leave2 = item">
{{item.name}}
</div>
</div>
<div class="leave2" v-for="item2 in selectLeave1"
@click="leave3 = item2"
>
{{item2.name}}
</div>
<div class="leave3" v-for="item3 in selectLeave2">
{{item3.name}}
</div>
</div>
</div>
</template>
<script>
import CascaderItem from './cascaderitem.vue'
export default {
name: 'GuluCascader',
props: {
source: {
type: Array
}
},
data(){
return {
popoverVisibility: false,
leave2: null,
leave3: null
}
},
computed: {
selectLeave1(){
if(this.leave2){
return this.leave2.children
}
},
selectLeave2(){
if(this.leave3){
return this.leave3.children
}
}
},
components: {CascaderItem}
}
</script>
上面的代碼一開始通過點擊popover使leave顯示,leave里面分了三層div均践,遍歷第一層摩幔,點擊第一層里對應的內容或衡,比如浙江封断,把與浙江有關的數(shù)據(jù)添加到一個一開始為null的屬性里坡疼,然后通過計算屬性返回它里面的children,之后在第二層里遍歷它祖搓,同樣把當前下面的數(shù)據(jù)賦值給leave3拯欧,然后通過計算屬性返回這個數(shù)據(jù)下的children再次遍歷
- 改進:上面1的代碼雖然實現(xiàn)了我們的功能镐作,但是就像我們一開始說的我們根本不知道當前會有幾層數(shù)據(jù)该贾,所以我們還是需要通過遞歸的形式
- cascaderitem.vue
<template>
<div class="cascader-item">
<div class="left">
<div v-for="item in items" @click="leftSelected = item">
{{item.name}}
</div>
</div>
<div class="right" v-if="rightItems">
<gulu-cascader-item :items="rightItems"></gulu-cascader-item>
</div>
</div>
</template>
<script>
export default {
name: 'GuluCascaderItem',
props: {
items: {
type: Array
}
},
data(){
return {
leftSelected: null
}
},
computed: {
rightItems(){
if(this.leftSelected && this.leftSelected.children){
return this.leftSelected.children
}else {
return null
}
}
}
}
</script>
- cascader.vue
<template>
<div class="cascader">
<div class="popover" @click="popoverVisibility = !popoverVisibility">
</div>
<div class="leave" v-if="popoverVisibility">
<cascader-item :items="source"></cascader-item>
</div>
</div>
</template>
<script>
import CascaderItem from './cascaderitem.vue'
export default {
name: 'GuluCascader',
props: {
source: {
type: Array
}
},
data(){
return {
popoverVisibility: false,
}
},
components: {CascaderItem}
}
</script>
上面的代碼就是每次分為左邊區(qū)域跟右邊區(qū)域,然后你下次在你的右邊里再次顯示你的左邊逞力,依次類推寇荧,不斷的在右側調用你這個組件揩抡,也就是不斷的在右側顯示你的左側
讓用戶可以自定義高度
通過給cascader.vue傳入一個height峦嗤,然后在它中的props里聲明這個height寻仗,然后再在cascader.vue中把這個height傳給cascader-items.vue,在這里面設置style為你的height
- index.html
<g-cascader :source="source" height="200px"></g-cascader>
- cascader.vue
<div class="cascader">
<cascader-item :items="source" :style="{height}" :height="height"></cascader-item>
</div>
import CascaderItem from './cascader-items.vue'
props: {
source: {
type: Array
},
height: {
type: String
}
},
- cascader-items.vue
<template>
<div class="cascader-item">
<div class="left">
<div class="label" v-for="item in items" @click="leftSelected = item">
{{item.name}}
<icon name="right" v-if="item.children"></icon>
</div>
</div>
<div class="right" v-if="rightItems">
<gulu-cascader-item :items="rightItems" :style="{height}" :height="height"></gulu-cascader-item>
</div>
</div>
</template>
<script>
import Icon from './icon.vue'
export default {
name: 'GuluCascaderItem',
props: {
items: {
type: Array
},
height: {
type: String
}
},
重新點擊第一層的時候后面的第三層的層級隱藏掉
實現(xiàn)方法:通過傳一個selected數(shù)組,和一個level層級硝烂,默認selected是一個空數(shù)組滞谢,level為0狮杨,開始遞歸的時候level的值就等于level+1(也就是右側的層級是在左側的基礎上加1)橄教,然后點擊每一項的時候在selceted數(shù)組里的第level項的值為你點擊的這一項的item(這樣就可以保證每一層級在數(shù)組里只有一個值护蝶,不會每點擊一個添加一個)持灰,并且每次點擊的時候都把這個數(shù)組里的第level+1和之后的項全部刪掉搅方,然后通過子組件觸發(fā)父組件的@update:selected事件把一個新的深拷貝的selected傳給最外的父組件
- demo.vue
<template>
<div>
<div style="padding: 20px;">
<g-cascader :source="source" height="200px" :selected="selected"
@update:selected="selected = $event"
></g-cascader>
</div>
</div>
</template>
<script>
data(){
return {
source: [
{
name: '浙江',
children: [
{
name: '杭州',
children: [
{name: '上城'},
{name: '下城'},
{name: '江干'}
]
},{
name: '嘉興',
children: [
{name: '南湖'},
{name: '秀洲'},
{name: '嘉善'}
]
}
]
},
{
name: '福建',
children: [
{
name: '福州',
children: [
{name: '鼓樓'},
{name: '臺江'},
{name: '蒼山'}
]
}
]
}
],
selected: []
}
},
</script>
- cascader.vue
<div class="popover" v-if="popoverVisibility">
<cascader-item :items="source" :style="{height}" :height="height" :selected="selected" :level="level"
@update:selected="onUpdateSelected"
></cascader-item>
<script>
import CascaderItem from './cascader-items.vue'
props: {
selected: {
type: Array,
default: []
},
level: {
type: Number,
default: 0
}
},
computed: {
result(){
return this.selected.map(item=>{return item.name}).join('/')
}
},
components: {CascaderItem},
methods: {
onUpdateSelected(val){
this.$emit('update:selected',val)
}
}
</script>
- cascader-item.vue
<template>
<div class="cascader-item">
<div class="left">
<div class="label" v-for="item in items" @click="onSelected(item)" >
{{item.name}}
<icon name="right" v-if="item.children"></icon>
</div>
</div>
<div class="right" v-if="rightItems">
<gulu-cascader-item :items="rightItems" :style="{height}" :height="height" :selected="selected" :level="level+1"
@update:selected="onUpdateSelected"
></gulu-cascader-item>
</div>
</div>
</template>
<script>
import Icon from './icon.vue'
export default {
name: 'GuluCascaderItem',
props: {
items: {
type: Array
},
height: {
type: String
},
selected: {
type: Array,
default: ()=>{return []}
},
level: {
type: Number,
default: 0
}
},
data(){
return {
}
},
computed: {
rightItems(){
let currentSelected= this.selected[this.level]
if(currentSelected && currentSelected.children){
return currentSelected.children
}else {
return null
}
}
},
components: {
Icon
},
methods: {
onSelected(item){
let copy = JSON.parse(JSON.stringify(this.selected))
//之所以寫copy[this.level]是為了你點擊當前層的每一個都讓數(shù)組里只保留一個
//而不是點一個就往數(shù)組里加一個涛漂,如果不寫的話你點杭州數(shù)組里有一個杭州匈仗,再點福建
//數(shù)組就會變成['杭州','福建']可這兩個屬于同一層悠轩,我們統(tǒng)一層只想保留一個
copy[this.level]= item
copy.splice(this.level+1)
this.$emit('update:selected',copy)
},
onUpdateSelected(val){
this.$emit('update:selected',val)
}
}
}
實現(xiàn)動態(tài)數(shù)據(jù)層級選擇
首先從github上引入一個數(shù)據(jù)庫https://github.com/eduosi/district/blob/master/district-full.csv火架,然后把它通過JSON轉化工具轉化成JSON格式何鸡,創(chuàng)建一個本地db.js骡男,通過封裝一個promise實現(xiàn)傳入一個id獲取到相應的子級,然后通過外層傳入一個loadData函數(shù)拾稳,通過loadData獲取到你點擊的item熊赖,然后拿到對應的id震鹉,調用你的promise传趾,成功后通過一個回調渲染我們的頁面
- demo.vue
<template>
<div>
<div style="padding: 20px;">
<g-cascader :source.sync="source" height="200px" :selected.sync="selected"
:loadData="loadData"
></g-cascader>
</div>
</div>
</template>
data(){
return {
source: [
],
selected: [],
}
},
methods: {
loadData(node,fn){
let {id}=node
this.ajax(id).then((result)=>{
fn(result)
})
},
ajax(id=0){
return new Promise((resolve,reject)=>{
let result = db.filter(item=>item.parent_id === id)
result.map(node=>{
//如果數(shù)據(jù)庫里有對應的對象的id等于當前節(jié)點的id浆兰,說明當前節(jié)點有children
if(db.filter(item=>item.parent_id === node.id).length > 0){
node.isLeaf = false
}else{
node.isLeaf = true
}
})
setTimeout(()=>{
resolve(result)
},300)
})
},
},
created() {
this.ajax().then((result)=>{
this.source = result
})
}
}
- cascader.vue
<div class="popover" v-if="popoverVisibility">
<cascader-item :items="source" :style="{height}" :height="height" :selected="selected" :level="level"
<!--一直把這個loadData傳給cascader-item-->
@update:selected="onUpdateSelected" :loadData="loadData"
></cascader-item>
</div>
props: {
loadData: {
type: Function
}
},
computed: {
result(){
return this.selected.map(item=>{return item.name}).join('/')
}
},
components: {CascaderItem},
methods: {
onUpdateSelected(val){
this.$emit('update:selected',val)
//因為你每次點擊后都會把當前這個后面的都刪掉,所以當前這個就是數(shù)組的最后一個也就是val[val.length-1]
let lastVal = val[val.length-1]
//當前的數(shù)據(jù)如果是第一層的話直接可以通過item.id===id拿到蜕便,如果是第二層就會是一個二維數(shù)組轿腺,所以你需要分別針對有沒有children設置不同的函數(shù)獲取它們對應的值
let simplest = (children,id)=>{
return children.filter(item=>item.id === id)[0]
}
let complex = (children,id)=>{
let noChildren = []
let hasChildren = []
children.forEach(item=>{
if(item.children){
hasChildren.push(item)
}else{
noChildren.push(item)
}
})
//沒有children的只需要使用simplest就可以拿到當前的
let found = simplest(noChildren,id)
if(found){
return found
}else{
// 如果是有children我們先把它當做沒children的找一遍,然后再對它里面
// 的children的每一項使用complex方法找一遍
found = simplest(hasChildren, id)
if(found){
return found
}else{
for(let i = 0;i<hasChildren.length;i++){
found = complex(hasChildren[i].children,id)
if(found){
return found
}
}
return undefined
}
}
}
let updateSource = (result)=>{
//source是props所以需要深拷貝
let copy = JSON.parse(JSON.stringify(this.source))
//拿到你點擊元素的數(shù)據(jù)
let toUpdate = complex(copy,lastVal.id)
//給當前元素下面添加children值為你回調中獲取的當前點擊元素下的子元素
toUpdate.children = result
//觸發(fā)一個update:source將拷貝后的source傳出去
this.$emit('update:source',copy)
}
//如果最后一個的isLeaf是false并且this.loadData存在就去獲取數(shù)據(jù)
if(!lastVal.isLeaf && this.loadData){
//將你點擊的元素和updateSource這個回調傳進去
this.loadData(lastVal,updateSource)
}
}
}
- cascader-items.vue
<template>
<div class="cascader-item">
<div class="left">
<div class="label" v-for="item in items" @click="onSelected(item)" >
<span class="name">{{item.name}}</span>
<icon name="right" v-if="rightArrowVisible(item)"></icon>
</div>
</div>
<div class="right" v-if="rightItems">
<gulu-cascader-item :items="rightItems" :style="{height}" :height="height" :selected="selected" :level="level+1"
@update:selected="onUpdateSelected" :loadData="loadData"
></gulu-cascader-item>
</div>
</div>
</template>
props: {
items: {
type: Array
},
height: {
type: String
},
selected: {
type: Array,
default: ()=>{return []}
},
level: {
type: Number,
default: 0
},
loadData: {
type: Function
}
},
computed: {
rightItems(){
if(this.selected[this.level]){
let selected = this.items.filter((item)=>item.name === this.selected[this.level].name)
if(selected && selected[0].children&&selected[0].children.length > 0){
return selected[0].children
}
}
}
},
components: {
Icon
},
methods: {
rightArrowVisible(item){
//如果this.loadData存在的話也就是用動態(tài)數(shù)據(jù)坏平,那么就是!item.isLeaf的時候顯示箭頭功茴,否則就是item下有children顯示
return this.loadData ? !item.isLeaf : item.children
}
}
點擊外側關閉顯示層
方法1:點擊的時候添加一個docuemnt事件監(jiān)聽坎穿,因為會有冒泡玲昧,所以事件監(jiān)聽需要在this.$nextTick下寫,然后給這個事件監(jiān)聽的函數(shù)傳一個原生參數(shù)吕漂,拿到e.target根據(jù)它判斷屬不屬于cascader里面惶凝,如果屬于就什么都不干苍鲜,否則就關閉
<template>
<div class="cascader" ref="a" >
<div class="trigger" @click="toggle">
{{result || ' '}}
</div>
<div class="popover" v-if="popoverVisibility">
<cascader-item :items="source" :style="{height}" :height="height" :selected="selected" :level="level"
@update:selected="onUpdateSelected" :loadData="loadData"
></cascader-item>
</div>
</div>
</template>
documentClik(e){
let cascader = this.$refs.a
if(cascader.contains(e.target)){
return
}else{
this.close()
}
},
close(){
this.popoverVisibility = false
document.removeEventListener('click',this.toggle)
},
open(){
this.popoverVisibility = true
this.$nextTick(()=>{
document.addEventListener('click',this.documentClik)
})
},
toggle(){
if(this.popoverVisibility){
this.close()
}else{
this.open()
}
}
方法2:通過自定義指令給cascader最外層添加一個click-outside指令,實現(xiàn)點擊組件外關閉
- click-outside.js
export default {
// 當被綁定的元素插入到 DOM 中時……
bind: function (el, binding, vnode) {
document.addEventListener('click',(e)=>{
let {target} =e
if(el === target || el.contains(target)){
return
}
//拿到你指令中傳入的值坯屿,這里是close函數(shù)
binding.value()
})
}
}
- cascader.vue
<div class="cascader" v-click-outside="close">
</div>
import clickOutside from './click-outside.js'
directives: {clickOutside}
close(){
this.popoverVisibility = false
},
open(){
this.popoverVisibility = true
},
toggle(){
if(this.popoverVisibility){
this.close()
}else{
this.open()
}
}
問題:上面的寫法,如果有多個cascader就會有多個監(jiān)聽器
解決辦法:讓頁面剛加載的時候就添加一個監(jiān)聽事件隔节,聲明一個空數(shù)組怎诫,每次綁定指令的時候給這個數(shù)組里添加一個對象{el:el,callback:callback}幻妓,然后在最開始的監(jiān)聽事件里遍歷這個數(shù)組肉津,看看數(shù)組里面的每一項中是否有el===e.target或者el.containes(e.target)舱沧,如果有就直接return熟吏,否則就調用這一項的callback
- click-outside.js
let onClickDocument = (e)=>{
let {target} = e
arr.forEach(item=>{
if(item.el === target || item.el.contains(target)){
return
}
item.callback()
})
}
document.addEventListener('click',onClickDocument)
let arr = []
export default {
// 當被綁定的元素插入到 DOM 中時……
bind: function (el, binding, vnode) {
arr.push({el,callback:binding.value})
}
}
實現(xiàn)點擊數(shù)據(jù)未渲染完成時的loading狀態(tài)
實現(xiàn)方法:通過拿到在cascader里給cascader-item傳入一個loadingItem屬性悍引,一開始是一個空對象趣斤,然后點擊當前層級的某一項拿到selected數(shù)組里的最后一項賦值給這個loadingItem浓领,通過點擊的當前item.name等不等于loadingItem.name來判斷是否顯示loading联贩,如果相等就顯示否則就顯示箭頭,然后數(shù)據(jù)獲取成功在updateSource 中將loadingItem變?yōu)榭諏ο?/p>
- cascader.vue
<div class="popover" v-if="popoverVisibility">
<cascader-item :items="source" :style="{height}" :height="height" :selected="selected" :level="level"
@update:selected="onUpdateSelected" :loadData="loadData" :loadItem="loadItem"
></cascader-item>
</div>
data(){
return {
loadItem: {}
}
},
methods: {
onUpdateSelected(val){
let lastVal = val[val.length-1]
this.loadItem = lastVal
let updateSource = (result)=>{
this.loadItem={}
}
- cascader-item.vue
<div class="label" v-for="(item,index) in items" @click="onSelected(item,index)" :class="{active: currentItem === index}">
<span class="name">{{item.name}}</span>
<div class="icons" v-if="rightArrowVisible(item)">
<template v-if="loadItem.name === item.name">
<icon name="loading" class="loading"></icon>
</template>
<template v-else>
<icon name="right" class="next"></icon>
</template>
</div>
</div>
<div class="right" v-if="rightItems">
<gulu-cascader-item :items="rightItems" :style="{height}" :height="height" :selected="selected" :level="level+1"
@update:selected="onUpdateSelected" :loadData="loadData" :loadItem="loadItem"
></gulu-cascader-item>
</div>
props: {
loadItem: {
type: Object
}
}
實現(xiàn)點擊選中座菠,展開后仍然是對應的項添加選中狀態(tài)
實現(xiàn):首先要將我們可選擇的各省市區(qū)原來使用的v-if改為v-show浴滴,因為這樣的話才會不會去重新渲染升略,然后通過你之前點擊對應項把當前的項添加到selected里品嚣,然后通過當前的name來找selected里是否有這個name來判斷是否要添加這個選中的類翰撑,因為我們的selected里的item是對象,而對象沒法使用indexOf眶诈,所以這里我們單獨把selected里的name添加到一個新的數(shù)組里涨醋,之后判斷這個數(shù)組里是否存在我們當前項的name
- cascader-items.vue
<template>
<div class="left">
<div class="label" v-for="item in items" @click="onSelected(item)"
:class="{active: selectedName.indexOf(item.name) > -1}"
>
<span class="name">{{item.name}}</span>
</div>
</div>
</template>
data(){
return {
selectedName: []
}
},
methods: {
onSelected(item){
let copy = JSON.parse(JSON.stringify(this.selected))
//之所以寫copy[this.level]是為了你點擊當前層的每一個都讓數(shù)組里只保留一個
//而不是點一個就往數(shù)組里加一個,如果不寫的話你點杭州數(shù)組里有一個杭州逝撬,再點福建
//數(shù)組就會變成['杭州','福建']可這兩個屬于同一層浴骂,我們統(tǒng)一層只想保留一個
copy[this.level]= item
copy.splice(this.level+1)
this.selectedName = copy.map(item1=>{
//這時候this.selectedName=['杭州']
return item1.name
})
this.$emit('update:selected',copy)
},
}