background
For some business usage scenarios, the ideal interaction would be something like this.
Need to sort out
- Render operable and selectable cascade selectors directly instead of
antd
Similar to a selector in a form.
- Highly customized content
- different
tabs
Select linkage for cascading under - Custom node rendering
- Child node custom merge
- different
Thinking to comb
Since there is no suitable component for use, the original idea is to use it on the basis of ANTD through some SIMPLE secondary encapsulation of CSS and JS.
However…
The ANTD version of the cascade selector in the project does not support optional options.
I guess I’ll have to get my own. Let’s get this straight. It’s not that hard
At the core of logic: a node status checked | unchecked changes caused by changes in other states
- State changes for all children of the current node
- Changes to all ancestor nodes of the current node
The specific implementation
Props
We use the value+onChange parameter so that we can easily wrap it up with form elements. We also need the following parameters.
interface NodeItemProps {
/* Label name */
label: string
value: string
/* Child node */children? :Array<NodeItemProps>
/ * * / hierarchy
levelType: number
/* Parent value */
parentValue: stringrender? :(. arg:any) = > ReactElement
}
interface IProps {
/* Data source */
treeData: Array<NodeItemProps>
/* Initial value */value? :Array<string> onChange? :(list: Array<any>) = > voiddisableKeys? :Array<string> defaultExpend? :boolean
}
Copy the code
A hierarchy
The data source of a cascading selector is identical to the data source of a tree 🌲 component, except that it is different from the interaction and rendering of the tree: any node of the tree can be collapsed, shrunk, selected, etc., whereas the data source of a cascading selector only presents the data of the currently expanded node. So we only need to maintain a single piece of data that holds the currently expanded key.
For example, the state above, the first column is always present, click on the first column data (0105), in our maintenanceexpendKeys
To update the click value [0105], click 010501, update the ancestor node [0105,010501] that contains its own 010501, and so on. When we click a node, we need to update the current node and all the ancestor nodes of the current node.
So when we render, we will traverse the node and render childen under the node.
<Row type='flex'>
<Col>
<div>{treeData.map((node: NodeItemProps) => renderOneNode(node))}</div>
</Col>
{expendKeys.map((val, parentIndex) = > {
constlist = treeDataMap.get(val)? .children || []return (
<Col key={val}>
<div>
{list.map((node: NodeItemProps) => renderOneNode(node))}
</div>
</Col>
)
})}
</Row>
Copy the code
Node selected logic
What is more complicated is the influence of the current node on the ancestor node after it is selected.
Eg:
In the preceding scenario, if 01040201 is selected, all sibling nodes are selected. If parent node is selected, all sibling nodes are selected. If parent node is selected, parent node is selected.
So you need to get all the ancestor nodes of the node, and then iterate over the ancestor node to get the parent node that you want to select.
const parentKeys = getAllParent(node, treeData)
const appendParentKeys = []
/* Check whether the status of all parent nodes is selected after the current node is selected, and obtain the key of the parent node to be added
parentKeys.forEach(item= > {
const currentNode = treeDataMap.get(item)
constsublingKeys = currentNode? .children? .map(v= > v.value)
constisParentCheck = sublingKeys? .every(v= > [...value, item, ...appendParentKeys].includes(v))
isParentCheck && appendParentKeys.push(item)
})
Copy the code
The node is half-selected
All checkboxes are checked, i.e. checked, unchecked, half-checked, unselected, etc. Half-selected states are also easier to calculate
- If the child node is half-selected, the component node must be half-selected
- None of the children are selected, and none of them are unchecked. In English: selected part
const getIndeterminate = (node: NodeItemProps, keys: Array<string>) :boolean= > {
/* The child node is half-selected, and the ancestor node is half-selected
if (isEmpty(node.children)) return false
const isIncludeParent = keys.find(item= > {
returnnode.value ! == item && item.startsWith(node.value) })if (isIncludeParent) return true
constcheckList = node.children? .filter((item: NodeItemProps) = > {
return keys.includes(item.value)
})
constunCheckList = node.children? .filter((item: NodeItemProps) = > {
return! keys.includes(item.value) })return! isEmpty(checkList) && ! isEmpty(unCheckList) }// Get node status
const getStatusByNode = useCallback(
(node: NodeItemProps): NodeStatusProps= > {
const checked = allKeys.includes(node.value)
const indeterminate = getIndeterminate(node, value)
const disabled = disableKeys.includes(node.value)
return {
checked,
indeterminate,
disabled,
}
},
[value, disableKeys, allKeys]
)
Copy the code
other
As mentioned earlier, the antD version in the current project does not support the selected functionality. Combining Popover with a custom cascade selector, we can also implement a Cascader component similar to antD.
The idea is simple: wrap a layer of Popover to handle the display when the state of the hierarchy selector changes.
<div id='cascaderForm' className={cls(Style.container)}>
<Popover content={<DefCascader treeData={treeData} onChange={onCheckedChange} value={value} />} trigger='click'>
<section className={cls(Style.mockInput)}>
{value.length ? (
value.map((item: string, index: number) => {
if (index > maxCount) {
return null
}
if (index === maxCount) {
return (
<span className={cls(Style.baseTag)} key={item}>. +{value.length - maxCount}</span>
)
}
return (
<div className={cls(Style.baseTag)} key={item}>
{item}
<Icon
type='close'
className={cls(Style.closeIcon)}
onClick={e= > {
e.stopPropagation()
onDelete(item)
}}
/>
</div>)})) : (<span className={cls(Style.mockPlaceholder)}>{placeholder}</span>
)}
</section>
</Popover>
</div>
Copy the code
The last
In the process of implementation, there is an experience is: in the data calculation more complex scene, must extract some tool class pure function to deal with. In addition, Map structure is relatively easy to use.
For example, we can flat shoot the structure of a tree first and generate a Map, so that we can easily find nodes.
/* tree => tiled array */
@computed
get flatTags() {
return this.allTag.map((item: NodeItemProps) = > getNodesFromRoot(item)).flat()
}
/* Tag Map
@computed
get allTagCodeMap() {
return new Map(this.flatTags.map(it= > [it.value, it]))
}
/* Hierarchy grouping */
@computed
get groupKeys() {return lodash.groupBy(nodes, 'levelType')}Copy the code
The above components, while not as powerful as those in ANTD, are fine for some highly customized interactions and rendering scenarios.