今天我们来手写一个Cascader
,也就是层联选择器,一般长下面这样,通过点击输入框出来一个面板,可以层层选择数据,再次点击输入框或者外面,输入框会消失。
我们先准备一份数据
options: [{
value: 'zhinan',
label: '数码',
children: [{
value: 'shejiyuanze',
label: '手机',
children: [{
value: 'yizhi',
label: '苹果'
}, {
value: 'fankui',
label: '华为'
}, {
value: 'xiaolv',
label: '三星'
}, {
value: 'kekong',
label: '小米'
}]
}, {
value: '',
label: '笔记本电脑',
children: [{
value: 'cexiangdaohang',
label: 'Macbook Pro'
}, {
value: 'dingbudaohang',
label: 'iMac'
}]
}]
}, {
value: 'zujian',
label: '家电',
children: [{
value: 'basic',
label: '空调',
children: [{
value: 'layout',
label: '格力'
}, {
value: 'color',
label: '美的'
}]
},
{
value: 'basic',
label: '洗衣机',
children: [{
value: 'layout',
label: '西门子'
}, {
value: 'color',
label: '松下'
}]
},]
}
]
然后创建一个Cascader.vue
文件
<template>
<div class="container" v-click-outside="close">
<div class="input" @click="toggle"></div>
<div class="content" v-if="visiable">
<div class="content-left">
<div v-for="(item,index) in options" :key="index">
<div @click="select(item)">{{item.label}}</div>
</div>
</div>
<div class="content-right" v-if="list.length>0">
<div v-for="(item,index) in list" :key="index">
<div>{{item.label}}</div>
</div>
</div>
</div>
</div>
</template>
<script>
import clickOutside from "../../directives/outside";
export default {
props: {
options: {
type: Array,
default: () => []
}
},
data() {
return {
visiable: false,
selected: []
};
},
computed: {
list() {
return this.selected.children ? this.selected.children : [];
}
},
methods: {
toggle() {
this.visiable = !this.visiable;
},
select(item) {
console.log(item);
this.selected = item;
},
close() {
this.visiable = false;
}
},
directives: {
clickOutside: clickOutside
},
mounted() {}
};
</script>
可以看到,我们通过一个props
来接收要展示的数据,用v-for
循环展示这些数据,通过计算属性计算出点击左侧面板后右侧面板要显示的数据,通过visiable
属性搭配v-if
指令控制面板的显示和隐藏,同时这里我们使用一个自定义指令v-click-outside
来实现点击面板外面关闭面板,代码如下
const clickOutside = {
inserted: function (el, binding) {
function hide(e) {
if (el === e.target || el.contains(e.target)) {
return
}
binding.value()
}
el._hide = hide
document.addEventListener('click', el._hide)
},
unbind(el) {
document.removeEventListener(el._hide)
}
}
export default clickOutside
我们在页面组件引入Cascader
,将数据传给他,这时页面长这样
我们点击数码
,出现手机 笔记本电脑
这没毛病,但我们点击手机
的时候,并没有出现下一层。。。因为我们的dom结构只写了两个div,也就是class
名为content-left
和content-right
的这两个,想要显示第三层的话,就得再增加div,但你们只看到了第二层,而我看到了第五层。事实上我们不知道到底有多少层,所以我们得用Vue
的递归组件,我们再创建一个CascaderItem.vue
的文件,来循环右侧数据
<template>
<div class="panel">
<div class="content-left">
<div v-for="(item,index) in options" :key="index">
<div @click="select(item,index)" :class="{'selected':selectIndex==index}">{{item.label}}</div>
</div>
</div>
<div class="content-right" v-if="list.length>0">
<CascaderItem :options='list'></CascaderItem>
</div>
</div>
</template>
<script>
export default {
name:'CascaderItem',
props: {
options: {
type: Array,
default: () => []
}
},
data() {
return {
selected: [],
selectIndex:null
};
},
computed: {
list() {
return this.selected.children ? this.selected.children : [];
}
},
methods: {
select(item,index) {
console.log(item);
this.selected = item;
this.selectIndex = index
},
},
mounted() {}
};
</script>
好了 现在我们好像能展示多层数据了,但这时我们发现一个问题
我们这时点击家电,虽然第二层跟着变了,但第三层没有变。因为目前的写法第三层数据是由第二层数据点击产生的。那么如何解决这个问题呢?我们用在
Cascader
文件中声明一个数组 selectData
来存放我们选中的数据,为此我们还需要声明一个变量level
来表示当前层数
Cascader.vue
<template>
<div class="container" v-click-outside="close">
<div class="input" @click="toggle">{{inputVal}}</div>
<div class="content" v-if="visiable">
<CascaderItem :options='options' :selectData='selectData' :level='0' @change='change'></CascaderItem>
</div>
</div>
</template>
<script>
export default {
...
data() {
return {
...
selectData:[]
};
},
...
computed: {
inputVal(){
return this.selectData.map(item=>item.label).join('/')
}
},
methods: {
change(value,index){
this.selectData[index] = value
this.selectData.splice(index,1,value) //触发更新
},
},
};
</script>
CascaderItem.vue
<template>
<div class="panel">
<div class="content-left">
<div v-for="(item,index) in options" :key="index">
<div @click="select(item,index)" :class="{'selected':selected==item}">{{item.label}}</div>
</div>
</div>
<div class="content-right" v-if="list && list.length>0">
<CascaderItem :options='list' :selectData='selectData' :level='level+1' @change='change' ></CascaderItem>
</div>
</div>
</template>
<script>
export default {
..
props: {
..
selectData: {
type: Array,
default: () => []
},
level:{
type:Number
}
},
computed: {
list() {
return this.selectData[this.level] && this.selectData[this.level].children
}
},
methods: {
select(item,index) {
this.selected = item;
this.selectIndex = index
this.selectData.splice(this.level+1)
this.$emit('change',item,this.level)
},
change(item,index){
this.$emit('change',item,index)
}
},
};
</script>
可以看到,点击的时候我们把selectData
下一层之后的数据都删除了,这就符合了我们的逻辑,我们的右侧数据此时就可以通过selectData
和level
来得到了。这里我们要主要的是我们需要将level
传给递归组件,并且+1
,因为每递归一次,他的层数就会加一,此外,我们需要监听递归组件emit
出来的事件,然后再传递出去。
我们再稍作修改,将选择的数据传递出去
props:{
...
value: {
type: Array,
default: () => []
}
}
change(){
...
let selectArr = this.selectData.map(item => item.value);
console.log(selectArr);
this.$emit("input", selectArr);
}
这时,我们的静态层联基本就完成了
接下来我们来写异步的。。