手写封装select组件,参考elementUI源码

4,737 阅读1分钟

起因:

  • 最近做项目时使用了select标签,希望能更改select的样式,期望效果是select和option的背景都能透明,没有边框。期望效果是option也能透明,于是写了这样的css,
select {
  border-color: transparent;
  background: transparent;
  outline: none;
  color: #fff;
  option {
    color: black;
    opacity: 0;
    background-color: transparent;
    appearance: none;
  }
}
  • 效果并不理想,如图所示,option 的样式没有生效,网上也没有搜到解决方案,有人说option的样式没法修改,抓耳挠腮了好一阵,决定自己封装一个select标签组件。

封装简版select组件:

  • 这个版本的select是一个整体的组件,父组件传入数据数组,select组件依次展示。
    • select.vue
<template>
  <div id="myselelct">
    <div class="title" @click="showOption=!showOption">
      <span>{{ choiceTitle }}</span>
      <span class="icon"></span>
  </div>
  <div v-show="showOption">
    <div v-for="ch in options" :key="ch.key" @click="changeOption(ch.key)">
      {{ ch.text }}
    </div>
  </div>
  </div>
</template>
<script>
  export default {
    props: {
      options: {   // 接收父组件传入的所有选项
        type: Array,
        default() {
          return []; // 接收数据格式必须为:[{key:1,text:"hahha"}]
        },
      },
      value: {  // 接收父组件传入的当前值,名字必须value,这样父组件才能用v-model绑定
        type: String,
        default: "",
      },
    },
    data() {
      return {
        showOption: false,// 控制是否显示下拉选框
      };
    },
    computed: {
      choiceTitle() {  // 当前选中的title
        let curOption = this.options.filter(item=>item.key == this.value)
        if (curOption.length > 0) return curOption[0].text
        return "请选择";
      },
    },
    methods: {
      changeOption(val) {
        this.$emit("input", val); // 传给父组件的事件名必须是input,这样才能用v-model绑定
        this.showOption = false; // 收起下拉框
      },
    },
  };
</script>
<style lang="less" scoped>
  #myselelct {
    display: inline-block;
    padding: 5px;
    cursor: default;
    .title {
      .icon {
        margin-left: 5px;
        vertical-align: middle;
      }
    }
  }
</style>

使用:

  • 只需要传入所有选项(:options="type"),与当前选中项即可(v-model="choiceType")
<my-select class="myselect" :options="type" v-model="choiceType" />
...
<style lang="less" scoped>
  .myselect {
    position: absolute;
    z-index: 10;
    color: #fff;
  }
</style>
  • 这就实现预期效果了:

封装完select组件,突然想起elementui不就有这个组件吗,决定搞一个简版的el-slelect组件

参考element-ui,设计select与option组件

element ui的select使用

  • v-model中的value就是当前被选中的el-option的 value 属性值
<template>
  <el-select v-model="value" placeholder="请选择">
    <el-option
      v-for="item in options"
      :key="item.value"
      :label="item.label"
      :value="item.value">
    </el-option>
  </el-select>
</template>

难点与思路:

  1. 组件el-select里面需要插入el-option
    • 解决方案:使用插槽
  2. el-select如何获取插槽中el-option组件的数据,或者怎么判断当前选中的是哪一个选项
    • 两个组件是嵌套关系,并非父子关系,不能使用props,推荐使用provideinject
    • 将el-select的this传递给el-option,在el-option中就可以操作el-select
// provide和inject的简单使用,详细使用参考官网
// 外层组件
export default { 
  provide() {
    return {
      data: '这是外层组件的数据',
    };
  },
}
// 内层组件
export default { 
  inject: ["data"],
}
  1. 使用时需要v-model绑定
    • 这涉及到v-model原理,子组件在props接收一个value,并向父组件传递一个input自定义事件

代码实现:

select组件

  • ySelect.vue
<template>
  <div id="select" class="y-select" :style="{ width: width + 'px' }">
    <div class="title">
      <span v-if="!curLabel" class="placeholder">{{ placeholder }}</span>
      <span v-else>{{ curLabel }}</span>
      <span class="icon" @click="showOption"></span>
    </div>
    <div class="option" v-show="isShow">
      <slot></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    placeholder: {      // 暗字
      type: String,
      default: "请选择",
    },
    value: { // 接收父组件传入的value,名字必须value,这样父组件才能用v-model绑定
      type: [Number, String],
    },
    width: {  // 宽度
      type: Number,
      default: 200,
    },
  },
  // 向插槽中的组件传入数据,传入当前组件的this,在option组件中可以直接访问
  provide() {
    return {
      select: this,
    };
  },
  data() {
    return {
      isShow: false, // 是否下拉
      curLabel: "", // 当前选中的文字
      curValue: "", // 当前选中的value
      options: [],  // 存储插值中的所有option,在option组件的created时push
    };
  },
  mounted() {
    // 与父组件传入的value对比,确定当前选中的curvalue
    let curOption = this.options.filter((item) => item.value === this.value);
    if (curOption.length > 0) {
      this.curLabel = curOption[0].label;
      this.curValue = curOption[0].value;
    }
  },
  methods: {
    showOption() {   // 点击下拉按钮,收起/展开options
      this.isShow = !this.isShow;
    },
  },
  watch: {
    // 当前选中项变化,触发input事件,父元素既可以使用v-model监听
    curValue() {
      this.$emit("input", this.curValue);
    },
  },
};
</script>

<style scoped>
.y-select {
  color: #666;
  display: inline-block;
}
.title {
  display: flex;
  justify-content: space-between;
  padding: 3px 6px;
  border: 1px solid #666;
  color: #666;
}
.title span {
  /* 一行显示,超长省略号 */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.placeholder {
  color: #bbb;
}
.icon {
  cursor: pointer;
}
</style>

option组件

  • yOptions.vue
<template>
  <div id="option" class="y-option" ref="option">
    <div class="content" @click="clickItem">
      {{ label }}
    </div>
  </div>
</template>

<script>
export default {
  props: ["label", "value"], // label是选项文字,vaue是选项值
  inject: ["select"], // 接收select组件中的provide数据,"select"就是父组件的this
  created() {
    // 将自身的label和value,push到给select父组件的options中
    this.select.options.push({
      label: this.label,
      value: this.value,
    });
  },
  methods: {
    clickItem() {
      // 改变select中当前选中的元素
      this.select.curValue = this.value;
      this.select.curLabel = this.label;
      // 调用select中的方法,收起下拉选框
      this.select.showOption();
    },
  },
};
</script>

<style scoped>
.content {
  color: #666;
  border-right: 1px solid #666;
  border-left: 1px solid #666;
  border-bottom: 1px solid #666;
  padding: 5px 30px 5px 6px;
  text-align: left;
  cursor: pointer;
  /* 一行显示,超长省略号 */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
</style>

使用

  • app.vue
<template>
  <div id="app">
    <y-select v-model="myValue">
      <y-option
        v-for="item in options"
        :label="item.label"
        :value="item.key"
        :key="item.key"
      ></y-option>
    </y-select>
  </div>
</template>

<script>
import ySelect from "./components/ySelect.vue";
import yOption from "./components/yOption.vue";
export default {
  components: { yOption, ySelect },
  data() {
    return {
      myValue: "key5",
      options: [
        { key: "key1", label: "选项1" },
        { key: "key2", label: "选项2" },
        { key: "key3", label: "选项3" },
        { key: "key4", label: "选项4changhcadfjadfajhjfkjg" },
        { key: "key5", label: "选项5" },
        { key: "key6", label: "选项6" },
        { key: "key7", label: "选项7" },
      ],
    };
  },
};
</script>

效果图

关于样式修改,可以在父组件中,使用 .y-select 或 .y-option进行修改,这里就不作阐述了。