项目中经常会用到单选,多选颜色,选择品牌...各种选择,如下图所示,通常这种使用radio或者checkbox来做的话,通常需要修改大量的逻辑与原生样式,耗时耗力。我们干脆使用最原始的方式封装成组件,只关注每一个Item的渲染,把选中等逻辑放到组件内部完成。只需要在外部关注事件回调即可。
还是从组件的需求来说,我们期望的使用方式为:
<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层实现各种功能,从而达到抽象逻辑,组件各司其职的效果。
架构设计
实现
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>