偷师Element-ui,组件开发so easy

368 阅读1分钟

背景

avatar 需要按照UI稿开发一个可以手动选择和取消选择子项的功能,然后将用户选择的数据搜集到一个数组中。思考了一下,so easy!马上开始码代码。。。

<template>
  <div class="checkbox-btn" @click="hanldeChange" :class="{ 'checkbox-btn-active': isChecked }">
    <slot></slot>
    <!-- 我的icon图标 -->
    <h-icon class="iconfont icon-checkmark checkbox-btn-icon"></h-icon>
  </div>
</template>
<script>
export default {
  name: "checkboxBtn",
  props: {
    value: [String, Number],
    checkboxGroup: Array
  },
  computed: {
    isChecked() {
      return this.checkboxGroup.indexOf(this.value) > -1;
    },
  },
  methods: {
    hanldeChange() {
      // 告诉parent组件我改变了
      this.$emit("change", this.value, this.isChecked)
    }
  }
}
</script>
<style lang="scss" scoped>
.checkbox-btn {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 84px;
  height: 28px;
  font-size: 12px;
  color: #555B66;
  cursor: pointer;
  border-radius: 2px;
  border: 1px solid #DADFE6;
  &:hover {
    border-color:#3a8ee6;
  }
  &-icon {
    display: none;
  }
  &-active {
    &:after {
      position: absolute;
      right: 0;
      bottom: 0;
      content: "";
      width: 0;
      height: 0;
      border-top: 16px solid transparent;
      border-right: 16px solid #3484FF;
    }

    .checkbox-btn-icon {
      display: block;
      position: absolute;
      right: -1px;
      bottom: -10px;
      font-size: 12px;
      transform: scale(0.7);
      color: white;
      z-index: 1;
    }
  }
}
</style>
<template>
  <div class="wrapper">
    <checkbox-btn 
      v-for="item in taskStatusList" 
      :value="item.value" 
      @change="handleChange" 
      :key="item.value" 
      :checkboxGroup="statusForm.statusList"
      >
       {{ item.label }}
    </checkbox-btn>
  </div>>
</template>
<script>
import CheckboxBtn from "@/components/checkboxBtn.vue";

export default {
  data() {
    return {
      statusForm: {
        statusList: []
      }
    }
  },
  components: {
    CheckboxBtn
  },
  methods: {
    handleChange(value, isChecked) {
      let list = this.statusForm.statusList;
      if (isChecked) {
        list.splice(list.indexOf(value), 1);
      } else {
        list.push(value)
      }
    },
  }
}
</script>

代码实现了,可是一点都不优雅,每个子组件都需要传入list参数,都需要去监听change事件。每次引入组件外层都需要写逻辑来处理list数组的增删,对于组件使用者来说,我不关心这些,我只关心最后用户选择生成的数组。

是否有优化的方法?思考良久,这组件功能怎么这么眼熟啊。一番搜索,是的,[Element checkbox-group] (https://element.eleme.cn/#/zh-CN/component/checkbox)组件功能不就是一样样的吗,看来得“借鉴”一下,人家的组件是这样引用的:

  <template>
    <el-checkbox-group v-model="checkboxGroup1">
      <el-checkbox-button v-for="city in cities" :label="city" :key="city">{{city}}</el-checkbox-button>
    </el-checkbox-group>
  </template>

Element在外面包裹了一层group组件,我是不是也可以这样呢,肯定是可以的。问题的关键在于怎么更新父组件的数据,子组件emit吗,那外层组件不是又要监听这个事件,有没有更easy的方法。我们先来揭开v-model这个指令的语法糖:

Vue官网: 一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件。

思考良久,是不是触发group组件的input事件就行了呢。赶紧再去“借鉴一下”,于是我们的改良版组件就出来了:

<template>
  <div class="checkbox-btn-group">
    <slot></slot>
  </div>
</template>
<script>
  // group组件
  export default {
    name: 'checkboxBtnGroup',
    componentName: 'CheckboxBtnGroup',
    props: {
      value: {},
    }
  }
</script>
<style lang="scss" scoped>
.checkbox-btn-group {
  display: flex;
  flex-direction: row;
}
</style>
<template>
  <div class="checkbox-btn" @click="hanldeChange" :class="{ 'checkbox-btn-active': isChecked }">
    <slot></slot>
    <h-icon class="iconfont icon-checkmark checkbox-btn-icon"></h-icon>
  </div>
</template>
<script>
// checkbox子组件
import Emitter from '@/utils/emitter';

export default {
  name: "checkboxBtn",
  props: {
    value: [String, Number]
  },
  mixins: [Emitter],
  computed: {
    isChecked() {
      if (Array.isArray(this._checkboxGroup.value)) {
        return this._checkboxGroup.value.indexOf(this.value) > -1;
      } else {
        return false
      }
    },

    _checkboxGroup() {
      let parent = this.$parent;
      while (parent) {
        if (parent.$options.componentName !== 'CheckboxBtnGroup') {
          parent = parent.$parent;
        } else {
          return parent;
        }
      }
      return false;
    },
  },
  methods: {
    hanldeChange() {
      this.$nextTick(() => {
        let result = this._checkboxGroup.value
        if (this._checkboxGroup) {
          if (this.isChecked) {
            result.splice(result.indexOf(this.value), 1);
          } else {
            result.push(this.value)
          }
          this.dispatch('CheckboxBtnGroup', 'input', [result]);
        }
      });
    }
  }
}
</script>
<style lang="scss" scoped>
.checkbox-btn {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 84px;
  height: 28px;
  font-size: 12px;
  color: #555B66;
  cursor: pointer;
  border-radius: 2px;
  border: 1px solid #DADFE6;
  &:hover {
    border-color:#3a8ee6;
  }
  &-icon {
    display: none;
  }
  &-active {
    &:after {
      position: absolute;
      right: 0;
      bottom: 0;
      content: "";
      width: 0;
      height: 0;
      border-top: 16px solid transparent;
      border-right: 16px solid #3484FF;
    }

    .checkbox-btn-icon {
      display: block;
      position: absolute;
      right: -1px;
      bottom: -10px;
      font-size: 12px;
      transform: scale(0.7);
      color: white;
      z-index: 1;
    }
  }
}
</style>

具体在页面中怎么引入我们开发好的组件呢,看下面,是不是优雅了很多。

<template>
  <checkbox-btn-group v-model="statusForm.statusList">
    <checkbox-btn v-for="item in taskStatusList" :value="item.value" :key="item.value">{{ item.label }}</checkbox-btn>
  </checkbox-btn-group>
</template>

我们发现在上面多了一个dispatch方法,这又是何方神圣:

export default {
  methods: {
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;

      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    }
  }
};

这就是Element传说中自己实现的dispatch方法,具体的作用就是不断向上查找,直到找到跟变量componentName名字相同的parent组件,然后触发事件变量eventName事件。我们这里触发的就是input事件,通过v-model的语法糖,我们更新了group组件上绑定的数组,至此整个完整的组件就完成了。

总结

我们通过dispatch方法,在子组件封装了外层数组更新的逻辑,利用v-model的语法糖,更新了group组件的数据。在组件引入时,最外面所需的只要传入我们的数组即可。

彩蛋

由于文章篇幅的限制,大家自己去看看element checkbox-button 和 checkbox-group的源码,看看大家发现了啥(大家观察它的model),欢迎大家到评论中来讨论。

欢迎关注

码字实属不易,希望大家能关注一波公众号,一起学习,一起Easy。

avatar