记一个高度复用性单选-多选(非radio,checkbok)组件的开发

524 阅读3分钟

项目中经常会用到单选,多选颜色,选择品牌...各种选择,如下图所示,通常这种使用radio或者checkbox来做的话,通常需要修改大量的逻辑与原生样式,耗时耗力。我们干脆使用最原始的方式封装成组件,只关注每一个Item的渲染,把选中等逻辑放到组件内部完成。只需要在外部关注事件回调即可。

img

还是从组件的需求来说,我们期望的使用方式为:

 <CheckGroup class="selector" @onSelect="onSelect" deriction="x | y">
    <CheckItem :renderItem="renderColorItem"/>
    ...
</CheckGroup>

onSelect:function(ids){
    //用户选择的项目id列表
    console.log(ids)
}

眼尖的同学可能看到了,我们的CheckGroup与CheckItem的使用方式是不是很类似于iview或者element的Form组件?没错,我就是借鉴了Form的实现方式,将FromItem的事件处理(依赖收集,校验等等)放到Form顶层,通过Provide将数据注入到FormItem中,表单元素事件通过eventBus传递到FormItem,最终到达From,在Form层实现各种功能,从而达到抽象逻辑,组件各司其职的效果。

架构设计

image

实现

CheckGroup实现很简单,就做了以下几件事。

  • 1 收集所有的Item(我的项目只是收集到父容器,没有使用,为了以后高级功能使用)。
  • 2 通过Provide给子组件顶层注入数据。
  • 3 通过eventBus处理事件,接收子元素点击等等事件数据

首先在template层,通常我们多项选择都是放到scrollView(基于batter-scroll)的,因此我们需要对不同的方向渲染不同的样式,以使得外层scrollview可以顺利滚动 batter-scroll文档传送门

<template>
  <div class="multiCheck-container" :style="direction=='x' ? xStyle : yStyle">
    <slot name="default"></slot>
  </div>
</template>
export default {
  props: {
    multiCheck: {
      type: Boolean,
      required: false,
      default: true
    }
  },
  data () {
     return {
      itemsGroup: [],
      userSelect: {
        id: [],
        item: []
      }, // 用户选择的item
      xStyle: {
        // 使得容器宽度内部可以被横向撑开
        display: 'inline-block',
        width: 'auto',
        whiteSpace: 'nowrap'
      },
      yStyle: {
        display: 'flex',
        'flex-direction': 'row',
        'flex-wrap': 'wrap',
        width: '100%'
      }
    }
  },
  // 向子组件注入数据
  provide () {
    return {
      choseData: this.userSelect,
      chooseForm: this
    }
  },
  created: function () {
    // 初始化添加item
    this.$on('on-add-item', item => {
      this.itemsGroup.push(item)
    })
  },
  mounted: function () {
    this.$on('on-click-item', item => {
      if (this.multiCheck) {
        // 多选
        const hasChecked = this.userSelect.id.some(v => v === item.id)
        if (hasChecked) {
          // 二次点击删除项目
          let index = this.userSelect.id.findIndex(v => v === item.id)
          this.userSelect.id.splice(index, 1)
          this.userSelect.item.splice(index, 1)
        } else {
          // 一次点击添加项目
          this.userSelect.id.push(item.id)
          this.userSelect.item.push(item)
        }
      } else {
        // 单选
        this.userSelect.id = item.id
        this.userSelect.item = item
      }
      //通过emit通知外部选择的项目
      this.$emit('onSelect', this.userSelect)
    })
  }
}

实现CheckItem

在CheckItem中我们需要做的事情有

  • 1 通过eventBus将子元素添加进父元素依赖中
  • 2 通知父组件子元素被点击,同时携带当前点击的item信息传递到父checkgroup组件

checkItem.js

<template>
  <div class="multicheckItem" :style="{display:FormParent.multiCheck?'inline-block':'block'}">
    <Render :render="renderItem" :bindData="{onItemClick,choseData,FormParent}" />
  </div>
</template>
import Render from '../render'
export default {
  components: { Render },
  props: {
    renderItem: {
      type: Function,
      required: true
    }
  },
  inject: {
    FormParent: {
      from: 'chooseForm',
    },
    choseData: {
      from: 'choseData',
    }
  },
  // 通知父组件收集子元素实例
  created: function () {
    this.FormParent.$emit('on-add-item', this)
  },
  methods: {
    onItemClick: function (item) {
      this.FormParent.$emit('on-click-item', item)
    }
  }
}

为了我们更容易扩展,我们把子元素的渲染交给一个函数式组件,这样我们在调用checkout组件的时候只需要传入一个名为renderItem的方法,并且返回任意元素出来,他会携带onItemClick,choseData,FormParent这三个参数过去,我们只需要在renderItem方法中渲染的item组件中处理click事件以及选中后的样式就ok啦

Render.js

export default {
  name: 'Render',
  functional: true,
  props: {
    render: [Function, String],
    bindData: [Object]
  },
  render: function (h, ctx) {
    return ctx.props.render(h, ctx.props.bindData)
  }
}

举个栗子


<CheckGroup class="selector" @onSelect="onSelect">
    <CheckItem
    v-for="(item,index) in ColorList"
    :renderItem="(h,bindData)=>renderItem(h,bindData,item,index)"
    />
</CheckGroup>

...省略部分代码

const ColorList =
[
    {
        id:1,
        color:'#222',
        name:'浅黑色'
    },
    {
         id:1,
        color:'#F00',
        name:'红色'
    },
    {
         id:1,
        color:'#0F0',
        name:'绿色'
    }
]
renderItem: function (h, bindData, item,index) {
  return <ColorItem bindData={bindData} item={item} />
},

那么在我们的ColorItem组件中:

ColorItem.js

<template>
  <div class="brand_item" @click="onBrandClick">
    <div
      class="itemimg"
      //匹配到id后增加一个背景色,表示选中
      :style="{background:bindData.choseData.id.some(v=>item.id == v) ? '#555' :''}"
    ></div>
    <p>{{item.name}}</p>
  </div>
</template>

<script>
export default {
  props: {
    item: {
      type: Object,
      required: true
    },
    bindData: {
      type: Object
    }
  },
  mounted: function () {},
  methods: {
    onBrandClick: function () {
      this.bindData.onItemClick(this.item)
    }
  }
}
</script>