小知识点:
- 如果你的文件是不需要编译的最后文件名前加一个下划线(_),比如_var.scss
2.vue的属性不能以data开头,否则转成默认属性
比如:datasource只能写成source
3.如果你在当前组件中使用了与你name相同的标签,那么name就是你当前的组件
4.我们不知道要渲染的数据有多少层,我们该怎么用v-for遍历?
比如有些区下面有镇,有的没有,也就是说无法确定当前数据数组有几层的情况下,那么我们可以通过递归组件让组件(通过组件自己调用自己来实现)
小案例
- 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数组,
然后遍历最开始的数组,拿到数组里面第一层的每一项,之后通过递归组件cascaderitem.vue接受你拿到的数组第一层的每一项(对象),显示你这每一项里面的name,然后调用自己再次进行遍历判断你传进来的sourceItem.children是否存在,如果存在就继续遍历souceItem.children,把拿到的新的对象item传给souceItem,然后再次显示这里面每一项的name,直到对应的item下面的children不存在为止。这里要注意递归组件自己本身要传入的属性,在自己内部也要再写一遍,就像上面的:sourceItem="item"
以上面的案例为例,一开始在cascader.vue组件里传入的是最开始的数据,然后遍历分别拿到的item是
{
name: '浙江',
children: [
{
name: '杭州',
children: [
{name: '上城'},
{name: '下城'},
{name: '江干'}
]
},{
name: '嘉兴',
children: [
{name: '南湖'},
{name: '秀洲'},
{name: '嘉善'}
]
}
]
},
{
name: '福建',
children: [
{
name: '福州',
children: [
{name: '鼓楼'},
{name: '台江'},
{name: '苍山'}
]
}
]
}
上面两个对象,之后分别调用递归组件把这两个对象作为item依次传进去,开始执行cascaderitem.vue组件里的代码,以第一个浙江的为例,拿到{{sourceItem.name}}也就是浙江,然后执行下面的代码
<gulu-cascader-item v-for="(item,index) in sourceItem.children"
v-if="sourceItem.children"
:sourceItem="item"
:key="index"
>
</gulu-cascader-item>
也就是把当前的组件里的代码再执行一遍,它先判断sourceItem.children是否存在,因为sourceItem.children是
{
name: '杭州',
children: [
{name: '上城'},
{name: '下城'},
{name: '江干'}
]
}
{
name: '嘉兴',
children: [
{name: '南湖'},
{name: '秀洲'},
{name: '嘉善'}
]
}
所以再次对上面的对象也就是sourceItem.children再次进行遍历,得到的item分别为上面的两个,然后把这两个赋值给sourceItem,也就是sourceItem分别是杭州和嘉兴下面的对象,然后再分别使用这个组件,如此循环,最终得到下面结构
- 属性不要以show开头,因为以show开头的都是函数
页面渲染初步实现
- 正常情况下在已知有几层数据的情况下的视图渲染这里以上面的三层为例
<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,遍历第一层,点击第一层里对应的内容,比如浙江,把与浙江有关的数据添加到一个一开始为null的属性里,然后通过计算属性返回它里面的children,之后在第二层里遍历它,同样把当前下面的数据赋值给leave3,然后通过计算属性返回这个数据下的children再次遍历
- 改进:上面1的代码虽然实现了我们的功能,但是就像我们一开始说的我们根本不知道当前会有几层数据,所以我们还是需要通过递归的形式
- 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>
上面的代码就是每次分为左边区域跟右边区域,然后你下次在你的右边里再次显示你的左边,依次类推,不断的在右侧调用你这个组件,也就是不断的在右侧显示你的左侧
让用户可以自定义高度
通过给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
}
},
重新点击第一层的时候后面的第三层的层级隐藏掉
实现方法:通过传一个selected数组,和一个level层级,默认selected是一个空数组,level为0,开始递归的时候level的值就等于level+1(也就是右侧的层级是在左侧的基础上加1),然后点击每一项的时候在selceted数组里的第level项的值为你点击的这一项的item(这样就可以保证每一层级在数组里只有一个值,不会每点击一个添加一个),并且每次点击的时候都把这个数组里的第level+1和之后的项全部删掉,然后通过子组件触发父组件的@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]是为了你点击当前层的每一个都让数组里只保留一个
//而不是点一个就往数组里加一个,如果不写的话你点杭州数组里有一个杭州,再点福建
//数组就会变成['杭州','福建']可这两个属于同一层,我们统一层只想保留一个
copy[this.level]= item
copy.splice(this.level+1)
this.$emit('update:selected',copy)
},
onUpdateSelected(val){
this.$emit('update:selected',val)
}
}
}
实现动态数据层级选择
首先从github上引入一个数据库https://github.com/eduosi/district/blob/master/district-full.csv,然后把它通过JSON转化工具转化成JSON格式,创建一个本地db.js,通过封装一个promise实现传入一个id获取到相应的子级,然后通过外层传入一个loadData函数,通过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=>{
//如果数据库里有对应的对象的id等于当前节点的id,说明当前节点有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)
//因为你每次点击后都会把当前这个后面的都删掉,所以当前这个就是数组的最后一个也就是val[val.length-1]
let lastVal = val[val.length-1]
//当前的数据如果是第一层的话直接可以通过item.id===id拿到,如果是第二层就会是一个二维数组,所以你需要分别针对有没有children设置不同的函数获取它们对应的值
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))
//拿到你点击元素的数据
let toUpdate = complex(copy,lastVal.id)
//给当前元素下面添加children值为你回调中获取的当前点击元素下的子元素
toUpdate.children = result
//触发一个update:source将拷贝后的source传出去
this.$emit('update:source',copy)
}
//如果最后一个的isLeaf是false并且this.loadData存在就去获取数据
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存在的话也就是用动态数据,那么就是!item.isLeaf的时候显示箭头,否则就是item下有children显示
return this.loadData ? !item.isLeaf : item.children
}
}
点击外侧关闭显示层
方法1:点击的时候添加一个docuemnt事件监听,因为会有冒泡,所以事件监听需要在this.$nextTick下写,然后给这个事件监听的函数传一个原生参数,拿到e.target根据它判断属不属于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指令,实现点击组件外关闭
- 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函数
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就会有多个监听器
解决办法:让页面刚加载的时候就添加一个监听事件,声明一个空数组,每次绑定指令的时候给这个数组里添加一个对象{el:el,callback:callback},然后在最开始的监听事件里遍历这个数组,看看数组里面的每一项中是否有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})
}
}
实现点击数据未渲染完成时的loading状态
实现方法:通过拿到在cascader里给cascader-item传入一个loadingItem属性,一开始是一个空对象,然后点击当前层级的某一项拿到selected数组里的最后一项赋值给这个loadingItem,通过点击的当前item.name等不等于loadingItem.name来判断是否显示loading,如果相等就显示否则就显示箭头,然后数据获取成功在updateSource 中将loadingItem变为空对象
- 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
}
}
实现点击选中,展开后仍然是对应的项添加选中状态
实现:首先要将我们可选择的各省市区原来使用的v-if改为v-show,因为这样的话才会不会去重新渲染,然后通过你之前点击对应项把当前的项添加到selected里,然后通过当前的name来找selected里是否有这个name来判断是否要添加这个选中的类,因为我们的selected里的item是对象,而对象没法使用indexOf,所以这里我们单独把selected里的name添加到一个新的数组里,之后判断这个数组里是否存在我们当前项的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]是为了你点击当前层的每一个都让数组里只保留一个
//而不是点一个就往数组里加一个,如果不写的话你点杭州数组里有一个杭州,再点福建
//数组就会变成['杭州','福建']可这两个属于同一层,我们统一层只想保留一个
copy[this.level]= item
copy.splice(this.level+1)
this.selectedName = copy.map(item1=>{
//这时候this.selectedName=['杭州']
return item1.name
})
this.$emit('update:selected',copy)
},
}