联动表单实现思路
2018-02-20 本文已影响792人
djyuning
Vue.js 下由很多优秀的 UI 框架,这些框架无一例外的都提供了表单控件,但提供联动下拉的基本没有,原因很简单,联动表单的数据比较灵活,统一进行封装意义不大。
手头的项目用到联动表单的地方比较多,如:地区联动、无限分类联动等,又鉴于使用的是自建数据,所以无法直接使用现有的组件替代。
重点
需求
- 支持多级联动效果;
- 支持使用给定值初始化;
- 灵活调用;
思路
- 使用
props
传入selected
实现给定值初始化; - 使用组件的
data
对象接管props
传入的selected
数据,避免修改props
中的值(Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。); - 鉴于 Prop 是单向绑定的 原则,父组件还应传入一个事件监听,如:
@change
调用设计:
<!-- 原生调用 -->
<my-select></my-select>
<!-- 事件接管 -->
<my-select @change="newChange"></my-select>
<!-- 使用给定值初始化 -->
<my-select selected="新闻,国内新闻" @change="newChange"></my-select>
实现
最终实现效果:
实现起来还是比较简单的,大纲如下:
- 方法
getChilds
:该方法可以按统一的格式获取数据,它接受一个所选文字参数,传递到后端进行数据获取,后端查找对应的数据并返回给前端; - 方法
selectChange
:该方法是select
表单的@change
监听,它传回 2 个参数,一个是选择的值,一个是操作表单的索引; - 数据
propsSelected
:解析selected
属性,打散为数组使用,在mounted
中,遍历解析好的参数,逐层获取对应的数据,如:selected
打散后的数据为['新闻','国内新闻']
;
上面是所需的一些方法和变量,下面是逻辑:
- 在
mounted
中使用propsSelected
渲染出所有可用的下拉表单(初始化),未给出默认值时,使用最原始的值获取第一层数据(getChilds
); - 使用
selectChange
组件内单个下拉表单的选择事件,每次选择时,更新propsSelected
中对应的值,删除表单索引后的值,即修剪propsSelected
内容,如果选择的是默认值值,调用change
事件,否则继续获取下一级数据,如果后端传回的数据为空,则直接调用change
事件,否则继续追加数据,调用change
事件。
最终的代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="//unpkg.com/iview/dist/styles/iview.css">
</head>
<body>
<div id="app">
<h1>{{title}}</h1>
<tp-select :selected="tempCategories" @change="newChnage"></tp-select>
<h4>{{tempCategories}}</h4>
<tp-select @change="newSelect"></tp-select>
<h4>{{tempSelect}}</h4>
</div>
<script type="text/javascript" src="https://cdn.bootcss.com/vue/2.5.13/vue.min.js"></script>
<script type="text/javascript" src="https://cdn.bootcss.com/vue-resource/1.3.4/vue-resource.min.js"></script>
<script type="text/javascript" src="//unpkg.com/iview/dist/iview.min.js"></script>
<script type="text/javascript">
Vue.component('tp-select', {
template: '<div><i-select class="picker" v-for="(data, key) in tempData" :key="key" v-model="propsSelected[key]" @on-change="selectChange(propsSelected[key], key)">'
+ '<i-option :value="defaultOption">{{defaultOption}}</i-option>'
+ '<i-option v-for="(cate, sub) in data" :key="`${key},${sub}`" :value="cate.name">{{cate.name}}</i-option>'
+ '</i-select></div>',
props: {
selected: String,
},
data() {
return {
defaultOption: '请选择',
tempData: [],
propsSelected: this.propsToArray('selected'),
};
},
methods: {
propsToArray(key) {
let item = this.$props[key] || this.defaultOption || '请选择';
return item.split(',');
},
getChild(parent, success, error) {
this.$http.get('./data.php?name='+ (parent ? parent : '')).then(res => {
if(success && success instanceof Function) {
success(res.body, res);
}
}, res => {
if(error && error instanceof Function) {
error(res);
return;
}
throw res;
});
},
selectChange(val, key) {
// 每次都应移除 key 之后的数据
this.tempData = this.tempData.slice(0, key + 1);
this.propsSelected = this.propsSelected.slice(0, key + 1);
// 如果选择的是 请选择 项,那么就没有继续发送请求的必要了
if (val === this.defaultOption ) {
this.$emit('change', this.propsSelected.toString());
return;
};
// 根据选择的值加载对应的子级
this.getChild(val, categories => {
// 如果后端没有返回数据,说明选择到此结束
if (!categories || categories.length <= 0) {
this.$emit('change', this.propsSelected.toString());
return;
};
// 更新数据,触发回调
this.tempData.push(categories);
this.propsSelected.push(this.defaultOption);
this.$emit('change', this.propsSelected.toString());
});
}
},
mounted() {
// 初始化
this.propsSelected.forEach((item, index) => {
// 当前的列表是由当前值的上一个值获取的
if(index >= 1) {
this.getChild(this.propsSelected[index - 1], categories => {
this.tempData.splice(index, 1, categories);
});
return;
}
// 如果传入的是 0,那么应该获取第一层数据用于生成第一个下拉表单
this.getChild(0, categories => {
this.tempData.splice(0, 1, categories);
});
});
}
});
new Vue({
el: '#app',
data(){
return {
title: '联动效果实现',
tempCategories: '科技,数码产品',
tempSelect: null,
};
},
methods: {
newChnage(changed) {
this.tempCategories = changed;
},
newSelect(changed) {
this.tempSelect = changed;
}
},
mounted() {}
});
</script>
</body>
</html>
父组件可以通过 @change
实时获取到新的选择的值并更新到本组件内,由于子组件中并未实际的操作 props
中传入的变量,所以,也不用担变量被修改。
示例用到了一个 PHP 文件,内容如下:
<?php
$parent = isset($_GET['name']) ? $_GET['name'] : null;
$category = array(
array(
'name' => '默认分类',
),
array(
'name' => '新闻',
'child'=> array(
array(
'name' => '国内新闻',
),
array(
'name' => '国际新闻',
),
),
),
array(
'name' => '科技',
'child'=> array(
array(
'name' => '业界',
),
array(
'name' => '手机',
),
array(
'name' => '数码产品',
),
),
),
array(
'name' => '游戏',
'child'=> array(
array(
'name' => '网络游戏',
),
array(
'name' => '电子竞技',
),
array(
'name' => '热门游戏',
),
),
),
);
if(!$parent || empty($parent)) {
echo json_encode($category);
exit;
}
$use = null;
foreach ($category as $item) {
if( $item['name'] === $parent ) {
$use = isset($item['child']) ? $item['child'] : NULL;
}
}
echo $use ? json_encode($use) : FALSE;
异步触发
如果请求数据使用的参数和界面显示使用的参数是一致的,那么应该不会存在异步更新问题。但是,看下面的例子:
Props 传入的 selected 参数:湖南省,株洲市,茶陵县,潞水镇
API 数据请求使用的参数是:CN03
那么问题出现了,我们的初始化流程就会变成:初始第一层数据 - 根据湖南省查找出对应的标号CN03 - 使用 CN03 作为参数继续获取数据 - 根据株洲市查找出对应的标号CN0302 - ····· ,但是,因为期间会涉及到数据请求延时,所以,这里可能就会出现错误,例如:循环到 茶陵县 的时候,株洲市 的数据还没加载,于是就会报错!!!解决方法是使用异步处理:
首先,建立一个 Promise
方法:
readAreaData(block, index) {
return new Promise(resolve => {
// 获取地区对应的标号 Array.find(***);
let area = this.getAreaNum(block, index);
// Ajax加载数据
this.loadAreaData(area, areas => {
if (areas.length > 0) {
this.areaTemp.splice(index + 1);
this.areaTemp.push(areas);
}
resolve();
});
})
},
然后,在初始化的时候,使用异步方法调用上面的 Promise
方法:
mounted() {
// 初始化遍历
let eachChecked = async () => {
for (let i = 0; i < this.checked.length; i++) {
await this.readAreaData(this.checked[i], i)
}
};
// 获取到第一层数据后,开始根据已选数据逐个加载下拉菜单
this.loadAreaData('CN', areaTop => {
this.areaTemp.splice(0, 1, areaTop);
eachChecked();
});
},