1.应用背景
项目技术栈背景: react + antd3.x
产品要求要有一个cascader多选的功能,类似于以下
功能看起来很简单,但是该功能只支持antd5.x的项目,但项目为antd3.x不支持该功能,所以需要重新封装一下该组件。
ps:如果是antd5的项目,可以直接使用,代码如下
ini
<Cascader
style={{ width: '100%' }}
options={options}
onChange={onChange}
multiple
maxTagCount="responsive"
/>
所以对于antd3.x的项目需要重新手写一下该组件,不能直接在此基础上进行封装
2.实现思路
首先先理一下基本的实现思路,可以分为两部分,第一部分为上面的选择框,里面存放初始的placeholder以及选择后的tags; 第二部分为下面的多选项处理,处理为联级多选的功能。
初始的时候 是实现了该功能,使用trigger进行包裹。上面是显示自己写的选择框是一个
,没有选中值得时候显示placeholder的值,当选中后显示选中的标签tags列表。
ini
<Trigger
popup={
<Popup {...props} onCancel={handleCancel} onConfirm={handleConfirm} />
}
/>........</Trigger>
下面的联级选择框是一个自定义组件,为Trigger组件的children
ini
<Trigger ..........>
<Selector
forwardRef={selectorRef}
onRemove={handleItemRemove}
onClear={handleClear}
{...props}
/>
</Trigger>
然后当我基本写完的时候才发现有一个问题,那就是因为这个选择框中的数据可能会较多且名称相似(十几条或几十条),产品肯定会需要模糊搜索功能,火速去钉钉上一问,果然这个功能是必须拥有的。此时就处于进退两难的底部,一方面是不可或缺的功能,另一方面是无法实现的功能。因为上面是一个
包裹的内容,所以就不好实现输入框的功能,只能加个输入框,那么输入框中还要放选中的标签,这个就是怎么想都不好实现。
还好就是这个应用背景是,第一级选择项只有两个,所以灵机一动,改用treeSelect实现
虽然没有cascader看起来轻巧,但是产品还是同意了。然后接下来又来了一个类似的需求,此时的父级可能有多个,所以必须要实现该功能了。逃是逃不过去的。
整理了一下思路,重新设计,头部可以使用封装好的select选择框,它已经封装好了input + tags的展示模式,要渲染的联级多选可以使用
dropdownRender属性,实现自定义的展示内容。
3.实现方式
主体是一个函数组件,react hooks + antd
返回一个Select
ini
return <Select
// maxTagCount={10}
showArrow
disabled={disabled}
getPopupContainer={triggerNode => triggerNode}
dropdownMatchSelectWidth={false}
mode="multiple"
value={showSelectValueLabel}
allowClear={true}
onDeselect={removeTag}
dropdownRender={menu}
className="mn-style-multiple-cascader-container"
placeholder={placeholder}
showSearch={true}
clearIcon={<Icon type="close-circle" onClick={handleClear}/> }
onSearch={changeInput}
onBlur={onBlur}
/>
底部的联级多选也就是上面的 dropdownRender={menu} 分为两部分的展示,一个是输入框中无值时,展示cascader联级多选样式,二是输入框中有值时,展示过滤后的选项(样式我是参考的antd5.x过滤的样式,如下)
没有搜索内容
ini
{!inputValue && caseTree.map((caseItem, index) => { // 没有搜索内容则展示
return <div className="cascader-container-levelone" key={index}>
{caseItem.map((item) => {
return <div className={`check-item ${item.value === activeSelected ? 'activeBackground' : ''}`} onClick={e => {
e.preventDefault()
// console.log(index, item,childrenKey, 123);
if (index === 0) { // 表示是父级元素
setActiveSelected(item.value) // 表示为当前点击的父元素,为其增加背景色标识
}
spreadNext(item[childrenKey], index)
}}
key={item.value}
onMouseDown={e => e.preventDefault()}>
<Checkbox
onClick={e => {
e.stopPropagation()
spreadNext(item[childrenKey], index)
if (index === 0) { // 表示是父级元素
setActiveSelected(item.value)
}
}}
checked={item.checked}
indeterminate={item.indeterminate} // 表示checkbox的半选状态checked-true,indeterminate-false,表示全选;checked-false,indeterminate-false表示为空;其他表示半选
onChange={e => onCheckChange(item, e.target.checked)}
>{item.label}</Checkbox>
{item.children && item.children.length > 0 && <Icon type="right" />}
</div>
})}
</div>
})}
有搜索功能时
ini
{ // 有搜索内容时展示过滤内容
inputValue && <div className="filter-container">
{
filterOptions.map((filteritem, index) => {
let currentObj
caseTree.forEach(item => {
item.forEach(item1 => {
// (item1.children) && (item1.value === 'fuzhou')
if (item1.children) {
item1.children.forEach(itemchild => {
if (itemchild.value === filteritem.value) {
currentObj = itemchild
}
})
}
})
})
return <div key={index} className="filter-items" onClick={e => e.preventDefault()} onMouseDown={e => e.preventDefault()}>
<Checkbox
onClick={e => { e.stopPropagation() }}
checked={currentObj.checked}
indeterminate={currentObj.indeterminate}
onChange={e => { onCheckChange(currentObj, e.target.checked) }}
>{filteritem.fatherLabel}/{filteritem.label}</Checkbox>
</div>
})
}
</div>
}
这边虽然代码不算很多,总体来说300来行以及样式代码,这边就挑一些重要的函数实现来说明一下吧,不贴全部代码了。
点击父级时,展开下一级
scss
// 展开下一级
const spreadNext = (children, index) => {
if ((index || index === 0) && caseTree.indexOf(children) === -1) { //caseTree options的树形结构,[[.....], [.....]]第一个数组为父级元素,第二个数组为此时展开的父级元素的子级数组
if (children && children.length > 0) {
caseTree.splice(index + 1, caseTree.length - 1, children)
setCaseTree([...caseTree])
} else {
caseTree.splice(index + 1, caseTree.length - 1)
setCaseTree([...caseTree])
}
}
}
递归options数据,为其打上相关标记
多选框有未选,全选,半选状态,所以需要 indeterminate , checked两个状态控制
ini
/**
* 递归option数据
* 标记数据树形层级 parent
* 打上初始状态 checked indeterminate
*/
const recursiveOpt = (nodeArr, parent) => {
nodeArr.forEach((node) => {
if (parent) {
node.parent = parent
}
node.indeterminate = false
node.checked = false // 是否被选中
if (selectedValues && selectedValues.some(val => val === getLevel(node, 'value'))) {
node.checked = true
}
markChildrenChecked(node)
markParentChecked(node)
if (hasArrayChild(node, childrenKey)) {
recursiveOpt(node[childrenKey], node)
}
})
}
另外调试cascader样式的时候,只要一点击弹窗就会关闭,所以需先在点击事件中增加debbuger,防止弹窗关闭。
4.实现效果
最后 展示一下实现效果吧
模糊搜索: