联动表单实现思路

Vue.js 下由很多优秀的 UI 框架,这些框架无一例外的都提供了表单控件,但提供联动下拉的基本没有,原因很简单,联动表单的数据比较灵活,统一进行封装意义不大。

手头的项目用到联动表单的地方比较多,如:地区联动、无限分类联动等,又鉴于使用的是自建数据,所以无法直接使用现有的组件替代。

重点

需求

  • 支持多级联动效果;
  • 支持使用给定值初始化;
  • 灵活调用;

思路

调用设计:

<!-- 原生调用 -->
<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();
      });

    },
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容