本文实现级联组件须要用到自定义指令和组件通讯相关知识,最好先阅读如下两篇文章: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并遍历显示右侧列