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 ofantdSimilar to a selector in a form.

  • Highly customized content
    • differenttabsSelect linkage for cascading under
    • Custom node rendering
    • Child node custom merge

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 maintenanceexpendKeysTo 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.