手写一个Vue层联选择器

今天我们来手写一个Cascader,也就是层联选择器,一般长下面这样,通过点击输入框出来一个面板,可以层层选择数据,再次点击输入框或者外面,输入框会消失。

image

我们先准备一份数据

 options: [{
          value: 'zhinan',
          label: '数码',
          children: [{
            value: 'shejiyuanze',
            label: '手机',
            children: [{
              value: 'yizhi',
              label: '苹果'
            }, {
              value: 'fankui',
              label: '华为'
            }, {
              value: 'xiaolv',
              label: '三星'
            }, {
              value: 'kekong',
              label: '小米'
            }]
          }, {
            value: '',
            label: '笔记本电脑',
            children: [{
              value: 'cexiangdaohang',
              label: 'Macbook Pro'
            }, {
              value: 'dingbudaohang',
              label: 'iMac'
            }]
          }]
        }, {
          value: 'zujian',
          label: '家电',
          children: [{
            value: 'basic',
            label: '空调',
            children: [{
              value: 'layout',
              label: '格力'
            }, {
              value: 'color',
              label: '美的'
            }]
          }, 
          {
            value: 'basic',
            label: '洗衣机',
            children: [{
              value: 'layout',
              label: '西门子'
            }, {
              value: 'color',
              label: '松下'
            }]
          },]
        }
      ]

然后创建一个Cascader.vue文件

<template>
  <div class="container" v-click-outside="close">
    <div class="input" @click="toggle"></div>
    <div class="content" v-if="visiable">
      <div class="content-left">
        <div v-for="(item,index) in options" :key="index">
          <div @click="select(item)">{{item.label}}</div>
        </div>
      </div>
      <div class="content-right" v-if="list.length>0">
        <div v-for="(item,index) in list" :key="index">
          <div>{{item.label}}</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import clickOutside from "../../directives/outside";
export default {
  props: {
    options: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      visiable: false,
      selected: []
    };
  },
  computed: {
    list() {
      return this.selected.children ? this.selected.children : [];
    }
  },
  methods: {
    toggle() {
      this.visiable = !this.visiable;
    },
    select(item) {
      console.log(item);
      this.selected = item;
    },
    close() {
      this.visiable = false;
    }
  },
  directives: {
    clickOutside: clickOutside
  },
  mounted() {}
};
</script>

可以看到,我们通过一个props来接收要展示的数据,用v-for循环展示这些数据,通过计算属性计算出点击左侧面板后右侧面板要显示的数据,通过visiable属性搭配v-if指令控制面板的显示和隐藏,同时这里我们使用一个自定义指令v-click-outside来实现点击面板外面关闭面板,代码如下

const clickOutside = {
    inserted: function (el, binding) {
        function hide(e) {
            if (el === e.target || el.contains(e.target)) {
                return
            }
            binding.value()
        }
        el._hide = hide
        document.addEventListener('click', el._hide)

    },
    unbind(el) {
        document.removeEventListener(el._hide)
    }
}

export default clickOutside

我们在页面组件引入Cascader,将数据传给他,这时页面长这样

image

我们点击数码,出现手机 笔记本电脑这没毛病,但我们点击手机的时候,并没有出现下一层。。。因为我们的dom结构只写了两个div,也就是class名为content-leftcontent-right的这两个,想要显示第三层的话,就得再增加div,但你们只看到了第二层,而我看到了第五层。事实上我们不知道到底有多少层,所以我们得用Vue的递归组件,我们再创建一个CascaderItem.vue的文件,来循环右侧数据

<template>
    <div class="panel">
      <div class="content-left">
        <div v-for="(item,index) in options" :key="index">
          <div @click="select(item,index)" :class="{'selected':selectIndex==index}">{{item.label}}</div>
        </div>
      </div>
      <div class="content-right" v-if="list.length>0">
          <CascaderItem :options='list'></CascaderItem>
      </div>
    </div>
</template>

<script>
export default {
  name:'CascaderItem',
  props: {
    options: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      selected: [],
      selectIndex:null
    };
  },
  computed: {
    list() {
      return this.selected.children ? this.selected.children : [];
    }
  },
  methods: {
    select(item,index) {
      console.log(item);
      this.selected = item;
      this.selectIndex = index
    },
  },
 
  mounted() {}
};
</script>

image

好了 现在我们好像能展示多层数据了,但这时我们发现一个问题
image

我们这时点击家电,虽然第二层跟着变了,但第三层没有变。因为目前的写法第三层数据是由第二层数据点击产生的。那么如何解决这个问题呢?我们用在Cascader文件中声明一个数组 selectData 来存放我们选中的数据,为此我们还需要声明一个变量level来表示当前层数

Cascader.vue

<template>
  <div class="container" v-click-outside="close">
    <div class="input" @click="toggle">{{inputVal}}</div>
    <div class="content" v-if="visiable">
      <CascaderItem :options='options' :selectData='selectData' :level='0' @change='change'></CascaderItem>
    </div>
  </div>
</template>

<script>

export default {
 ...
  data() {
    return {
     ...
      selectData:[]
    };
  },
 ...
  computed: {
     inputVal(){
       return this.selectData.map(item=>item.label).join('/')
     }
  },
  methods: {
    change(value,index){
      this.selectData[index] = value
      this.selectData.splice(index,1,value) //触发更新
    },
   
  },
 
};
</script>

CascaderItem.vue


<template>
    <div class="panel">
      <div class="content-left">
        <div v-for="(item,index) in options" :key="index">
          <div @click="select(item,index)" :class="{'selected':selected==item}">{{item.label}}</div>
        </div>
      </div>
      <div class="content-right" v-if="list && list.length>0">
          <CascaderItem :options='list' :selectData='selectData' :level='level+1' @change='change' ></CascaderItem>
      </div>
    </div>
</template>

<script>

export default {
    ..
  props: {
   ..
     selectData: {
      type: Array,
      default: () => []
    },
    level:{
        type:Number
    }
  },
 
  computed: {
    list() {
      return this.selectData[this.level] && this.selectData[this.level].children 
    }
  },
  methods: {
    select(item,index) {
      this.selected = item;
      this.selectIndex = index
      this.selectData.splice(this.level+1)
      this.$emit('change',item,this.level)
    },
    change(item,index){
      this.$emit('change',item,index)
    }
  },
};
</script>

可以看到,点击的时候我们把selectData下一层之后的数据都删除了,这就符合了我们的逻辑,我们的右侧数据此时就可以通过selectDatalevel来得到了。这里我们要主要的是我们需要将level传给递归组件,并且+1,因为每递归一次,他的层数就会加一,此外,我们需要监听递归组件emit出来的事件,然后再传递出去。
我们再稍作修改,将选择的数据传递出去

props:{
...
 value: {
      type: Array,
      default: () => []
    }
}
 change(){
 ...
      let selectArr = this.selectData.map(item => item.value);
      console.log(selectArr);
      this.$emit("input", selectArr);
      }

这时,我们的静态层联基本就完成了


image

接下来我们来写异步的。。

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

推荐阅读更多精彩内容