阅读 508

使用v-model实现select组件

一、组件使用场景及需求分析

  • 表单多个固定单值的情况,我们不用再去input框输入值,直接在固定的值里面去选择
  • 选择以后父组件绑定的值对应改变,使得不需要发送表单前再进行赋值
  • 选择前后,列表都是不可见的

二、开始我们的coding

  • 首先我们需要的是一个有所有单值选项的list展示
  • 然后是一个展示当前选择的文字框

像这样的: 这一步我们只需要父组件传递单值代码,然后当前选中的一个值,没有的话就默认为空。

vue:

<div class="fd-select-box">
     <p v-text="scoped.selected&&scoped.selected.name?scoped.selected.name:'请选择'"></p>
     <span :class="fd-arrow icon iconfont">&#xe6a4;</span>
     <ul class="fd-select-list">
        <li v-for="(item,index) in list"
            :key="index+'select'"
            :class="{'active':scoped.selected&&item.code===scoped.selected.code}">
            {{item.name}}</li>
     </ul>
</div>
复制代码

JS:

 props: {
    list: {
      type: Array,
      required: true,
    },
    selected: Object,
  },
  data() {
    return {
      scoped: {
        // 当前选中的
        selected: this.selected,
      },
    };
  },
复制代码

CSS:

 .fd-select-box {
      position: relative;
      width: 200px;
      padding-right: 40px;
      padding-left: 10px;
      height: 36px;
      margin: 30px auto;
      line-height: 36px;
      border: 1px solid #41b883;
      border-radius: 4px;
      color: #000;
      font-size: 14px;
      text-align: left;
      cursor: pointer;
      box-sizing: border-box;

      .fd-arrow {
          position: absolute;
          top: 0;
          right: 0;
          font-size: 30px;
          transition: all 200ms;

          &.fd-down {
              transform: rotate(180deg);
          }
      }

      .fd-select-list {
          position: absolute;
          width: 100%;
          max-height: 200px;
          overflow: auto;
          list-style: none;
          top: 36px;
          left: 0;
          background: #fff;
          box-shadow: 0 0 5px rgba(0,0,0,0.2);
          z-index: 9;

          li {
              padding-left: 12px;
              line-height: 30px;
              cursor: pointer;

              &:hover {
                  background: rgba(65, 191, 138, 0.2);
              }

              &.active {
                  background: rgba(65, 191, 138, 0.9);
                  color: #fff;
              }
          }
      }
  }
复制代码

这样,我们已经把外层框架搭建好了。

接下来,解决子组件改变,父组件对应的值发生改变。

一般情况我们会想到子组件把当前选中的值通过this.$emit传给父组件,然后父组件再在对应方法里面给对应的值赋值。今天我们用另外一种方法来解决这个问题,那就是v-model,相信我,用了它你会爱上它。

好了,我们看看官方文档怎么说的:

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的model 选项可以用来避免这样的冲突:

  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
复制代码

cn.vuejs.org/v2/guide/co…

我的理解就是提供了v-model;在自定义组件上model里的prop里的字段的值会直接赋给props里面对应字段,像之前我们给checked传值是在父组件上通过:checked='false'这样一种形式。现在我们可以使用v-model='false'。来看具体在select框里面的表现吧。

export default {
  name: 'fdSselect',
  model: {
    prop: 'selected',
    event: 'changeValue',
  },
  props: {
    list: {
      type: Array,
      required: true,
    },
    selected: Object,
  },
  data() {
    return {
      scoped: {
        // 是否展示下面的列表
        showFlag: false,
        // 当前选中的
        selected: this.selected,
      },
    };
  },
  methods: {
    // 值改变后传给父组件,因为组件定义了model,所以父组件相当于执行了绑定的model值=emit出去的值
    changeValue(item) {
      this.scoped.selected = item;
      this.scoped.showFlag = false;
      this.$emit('changeValue', this.scoped.selected);
    },
  },
};
复制代码

父组件调用:

 <fd-select :list="selectList" v-model="selected"></fd-select>
复制代码

上面的event是我们要emit出去的事件名。这一步相当于在父组件执行了父组件的this.selected等于子组件的this.scoped.selected;所以其实你用组件的时候v-model="value" 其实就是 :value="value" @change="(val) => {value = val}"

现在看看我们实现的效果:

前两个需求已经实现了,最后一个需求是在交互上的优化。

首先他要一开始的时候不展示,我们给一个控制下拉框显隐的变量。showFlag默认值为false;点击输入框时展开下拉列表。然后选中选项后隐藏下拉列表。

注意我们的页面结构,下拉列表是输入框的子元素,所以点击下拉列表元素的时候会涉及到事件冒泡,这个时候我们使用.stop修饰符来组织时间冒泡导致下拉列表一直不能隐藏。 vue:

 <div class="fd-select-box" @click="changeShow">        
 	<p v-text="scoped.selected&&scoped.selected.name?scoped.selected.name:'请选择'"></p>        
    <span :class="['fd-arrow icon iconfont',{'fd-down':scoped.showFlag}]">&#xe6a4;</span>        
    <ul class="fd-select-list" v-show="scoped.showFlag">            
    	<li v-for="(item,index) in list"                
        	:key="index+'select'"                
            @click.stop="changeValue(item)"                
            :class="{'active':scoped.selected&&item.code===scoped.selected.code}">                
            {{item.name}}</li>        
     </ul>
 </div>
复制代码

JS:

// 值改变后传给父组件,因为组件定义了model,所以父组件相当于执行了绑定的model值=emit出去的值
    changeValue(item) {
      this.scoped.selected = item;
      this.scoped.showFlag = false;
      this.$emit('changeValue', this.scoped.selected);
    },
    // 改变下拉选项的显隐
    changeShow() {
      this.scoped.showFlag = !this.scoped.showFlag;
    },
复制代码

继续优化,我们现在实现了组件列表的显隐,但是只有操作当前组件时可以控制。那么我们点击其他地方的时候,其实也是希望组件列表可以隐藏起来的。

实现这个的思路:绑定一个点击事件在页面上,只要点击的元素不是当前组件,那么我们就可以隐藏当前组件的列表。这里我用到了自定义指令,具体实现如下:

clickOutside: {
    bind(el, binding) {
      function clickHandler(e) {
        // 这里判断点击的元素是否是本身,是本身,则返回
        if (el.contains(e.target)) {
          return false;
        }
        // 判断指令中是否绑定了函数
        if (binding.expression) {
          // 如果绑定了函数 则调用那个函数,此处binding.value就是handleClose方法
          binding.value(e);
        }
        return true;
      }
      // 给当前元素绑定个私有变量,方便在unbind中可以解除事件监听
      el.vueClickOutside = clickHandler;
      document.addEventListener('click', clickHandler);
    },
    unbind(el) {
      // 解除事件监听
      document.removeEventListener('click', el.vueClickOutside);
      delete el.vueClickOutside;
    },
  },

复制代码

最后实现效果如图:

后期待优化:实现可搜索的下拉框-->实现可以远程搜索的下拉框

以上为个人编写,希望能对大家的项目有所帮助,如有不当以及有更好的方法欢迎交流。

项目地址: github.com/jasminezx/s…