从零开始实现一个Vue级联组件

从零开始实现一个Vue级联组件

本文实现级联组件需要用到自定义指令组件通信相关知识,最好先阅读以下两篇文章:

Vue自定义指令

Vue组件基础与通信

一、组件简介

本文实现的是一个省、市、县...多级联动组件,当组件渲染完成后默认会加载出所有的省名称,当用户点击某个省的名称后,右边会自动添加一列显示该省下对应的市名称列表,当用户点击某个市后,右边又会自动添加一列显示该市下对应的县名称列表,同时支持级联列表的打开和关闭。
从零开始实现一个Vue级联组件_第1张图片

二、组件实现设计思路

① 组件所需要的数据,数据结构非常简单,对象里面只有两个属性,一个是label(标签名),如果当前标签下还有子标签,则会多一个children属性,children属性值为一个数组每个数组元素为其下的一个子标签

// data.json, 为避免数据占用太多篇幅,这里只列举了一条数据

[
    {
        "label": "江西",
        "children": [
            {
                "label": "赣州",
                "children": [
                    {
                        "label": "全南县"
                    },
                    {
                        "label": "龙南县"
                    }
                ]
            }
        ]
    }
]

② 我们的级联组件分为上下两部分组件,上部分显示用户选择的路径,下部分显示用户选择列表,同时支持点击级联组件的上部分可以实现下半部分的打开和关闭,点击组件外面关闭组件的下半部分,这里需要用到v-click-outside指令,这里自定义指令的代码就不再重复,请参考Vue自定义指令
// Cascader.vue 新建一个Cascader.vue组件



注意到组件中有一个 selectedItems数据,这是一个 数组,默认值为 空数组,因为当级联组件渲染完成后,默认用户是没有点击选择其中任何一项的,只有当 用户点击了某一项后,才会将 点击的这一项添加到selectedItems数组中,其就是 记录用户的选择项。这里需要理解清楚选择项的概念:
比如我们的级联组件有三列, 省、市、县三列,结合上面的数据结构,整个省是一个大对象,即 省对象,省对象中有children属性,里面包括多个子对象,即 市对象,市对象中又包括children属性,里面包括多个子对象,即 县对象,县对象中不再有children了,具体表示就是:
省对象:
{ "label": "江西", children: [省略...]}
市对象:
{ "label": "赣州", children: [省略...]}
县对象:
{ "label": "全南县"}
当用户点击 第一列,那么就 将整个省对象添加到selectedItems数组中的 第一项位置,当用户接着点击了 第二列,如省对象中的label为"赣州"的市对象,则 将整个市对象添加到selectedItems数组中的 第二项位置,当用户又点击了 第三列,如"赣州"市对象下的label为"全南县"的县对象,则 将整个县对象添加到selectedItems数组中的 第三项位置,这样selectedItems数组中就保存了用户选择的三列数据了,然后 将三列数据中的label取出通过"/"连接起来,即用户的选择路径"江西/赣州/全南县"。

③ 接下来就是考虑组件拿到数据后,如何渲染的问题了
这里需要用到组件内递归组件,我们可以左右两列抽象成一个单独的组件CascaderItem.vue,但是右边这一列会不会显示,得看用户有没有选择左边的项,如果点击了左边的项则显示右边的列,如果没有点击左边的项则不显示右边的列。

还是以省、市、县三列为例, 中间的市这一列,既是省的右列,也是县的左列,我们已经将左右两列抽象了一个单独的CascaderItem组件,关键是理解 省这一列的右边部分到底是什么?,从表面上看,省这一列的右边就是一个市列,但是如果右边仅仅是市这一列的话,那么 当用户点击市这一列中的某项的时候,就无法显示市右边的县列了,所以 省这一列的右边其实又是一个CascaderItem组件,只有这样点击市列中的某一项的时候,其右边的县列才会显示出来。所以我们需要 在CascaderItem组件内递归自己,而组件内递归自己,那么 必须给组件添加name属性,即 给组件取一个名字,如:

// CascaderItem.vue



CascaderItem组件组件的渲染数据来自于顶层父组件Cascader中的selectedItems数据,因为用户点击了左侧列中的项后, 会将点击的item项添加到selectedItems中,selectedItems中数据变化之后才会显示右侧的列。
CascaderItem组件需要接收一个 level属性,用来记录当前CascaderItem组件所属层级,即第几列,为了方便,我们 从0开始表示第一列,即第一层所以Cascader.vue中level传入0, 后面没加一层level会加1,如:

// 补全上面的Cascader.vue,渲染出下半部分

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


三、总结

整个Cascader组件设计思路就是: 在顶层父组件Cascader中 添加一个selectedItems数组,用于保存用户点击的 level层级(列序号)和对应的 item对象,同时用于 生成用户的选择路径,当用户点击了CascaderItem组件的左侧列中某项后, 通过层层传递事件的方式通知顶层父组件Cascader对其数据进行更新,顶层父组件Cascader更新数据后,CascaderItem组件 从selectedItems中取出对应level的item对象,然后获取item的children并遍历显示右侧列

你可能感兴趣的:(javascript,vue.js)