Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Tree conduction #39566

Merged
merged 2 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/blog/check-conduct.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: Tree 的勾选传导
date: 2022-12-14
author: zombieJ
---

In the Tree or similar components (such as TreeSelect, Cascader), needs check function. It's unambiguous most of the time, but when a `disabled` node appears somewhere in the middle, it's worth talking about. This article will introduce the logic of check conduction in antd. It should be noted that in different scenarios, there will be various requirements, and antd has chosen the most commonly used check conduction logic. If you need a different custom style, you can implement it yourself through `checkStrictly`.

## Some conduction strategies

Before we start, let's establish a consensus. That is, when a node is `disabled`, it cannot be clicked `checked`. Then we take the following Tree structure as an example:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*eMq8S7Pq0lQAAAAAAAAAAAAADrJ8AQ/original)

Next, we check the root node `parent 1`, and analyze the similarities and differences of different check transmission strategies.

### All nodes will be checked

This is the most intuitive strategy, all nodes will be checked:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*QQp-R4EMteAAAAAAAAAAAAAADrJ8AQ/original)

You can immediately see the problem with this strategy, we mentioned earlier that `disabled` nodes are not allowed to be `checked`. But when the parent node is not `disabled`, its child nodes will be forcibly checked. This will cause the `disabled` node to "can" be checked, which is obviously unreasonable.

### All checkable nodes are checked

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*BzrZRbT1gCEAAAAAAAAAAAAADrJ8AQ/original)

From the checkbox interaction, it looks good, but it's not intuitive. After `parent 1` is checked, `leaf 2` is checked by conduction. But the middle node `parent 1-0` is not checked. At some deep enough level, this strategy can cause the user to be unaware that a check has been propagated:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*3mHLQZvTgWsAAAAAAAAAAAAADrJ8AQ/original)

When there is no scrolling, the user can't realize that the upper `disabled` is not checked, but the top is checked:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xTqPQbdX6B0AAAAAAAAAAAAADrJ8AQ/original)

### Check only reachable checkable nodes

This is also the current strategy of antd, when a node is checked, it will propagate upwards and downwards from the node until `disabled` stops. When there are multiple `disabled` in the node, they will each check the status management:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*EIK0Rbq92CMAAAAAAAAAAAAADrJ8AQ/original)

Conversely check `leaf 2`, it will not conduct:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Ytr9SrJUvD4AAAAAAAAAAAAADrJ8AQ/original)

The advantage of this strategy is that users can clearly see the selection process. Compared with the previous strategy, users only need a small area to understand the check logic in the scrolling scene.

## Some implementation details

Note: We only introduce simple conduction logic here. Please refer to [actual code](https://github.com/react-component/tree/blob/62e0bf0b91d86b6e42fee69870ada9a4640b6c6f/src/utils/conductUtil.ts) for real world apply. Some performance optimizations will also be done, such as skipping nodes that have been traversed through the cache mechanism.

### Check conduction

When a node is checked, we will add `key` to `checkedKeys`. We iterate over each `key` in the new `checkedKeys` for conduction checks. The first step will be conduction from top to bottom (in the example below we check `0-0`):

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*30UnR60SSD8AAAAAAAAAAAAADrJ8AQ/original)

We record the current node `0-0` and the transmitted `0-0-0` and `0-0-1`:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*jo7wQZVX9S0AAAAAAAAAAAAADrJ8AQ/original)

In the second step, we will conduct upwards from this node:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*k5hoSKM1OMYAAAAAAAAAAAAADrJ8AQ/original)

Similarly, record the node `0` that was passed on:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*yqBETbq8ugQAAAAAAAAAAAAADrJ8AQ/original)

When the parent node is checked, the parent node of the parent node may also be checked, so we need to continue to conduct upward until the root node or `disabled` node.

### Uncheck conduction

Same as above, we will perform conduction traversal up and down, and then remove the conduction node from `checkedKeys`. Therefore no further repetition.

## Finally

Before the early days of v3, we encountered that the `disabled` check of Tree has different appeals in different scenarios (and each of them is "reasonable" when viewing fragmented appeals), and when it is extracted for inspection, We found that these fragmented demands can conflict with each other. Therefore, we sorted out its transmission logic and chose the most intuitive strategy. Of course, if the current implementation does not meet the requirements, you can implement it yourself through `checkStrictly`.
79 changes: 79 additions & 0 deletions docs/blog/check-conduct.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: Tree 的勾选传导
date: 2022-12-14
author: zombieJ
---

在 Tree 组件以及类似的组件(如 TreeSelect、Cascader),都会需要勾选功能。在大部分情况下它都没有歧义,但是当中间的某个节点出现 `disabled` 节点时,这就值的讨论了。这篇文章会介绍 antd 中,勾选传导的逻辑。需要注意的是,在不同的场景下,会有各种不同的需求,antd 选择了其中最常用的一种勾选传导逻辑。如果你需要不同的定制款,可以通过 `checkStrictly` 来自行实现。

## 一些传导策略

在开始之前,我们先确定一个共识。那就是当一个节点被 `disabled` 后,它是不能被点击 `checked` 的。接着我们以如下 Tree 结构做示例:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*eMq8S7Pq0lQAAAAAAAAAAAAADrJ8AQ/original)

接下来我们勾选根节点 `parent 1`,并分析一下不同的勾选传导策略的异同。

### 所有的节点都会被勾选

这是最直观的一种策略,所有的节点都会被勾选:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*QQp-R4EMteAAAAAAAAAAAAAADrJ8AQ/original)

你立刻就会发现这种策略的问题所在,我们前面提到过 `disabled` 节点不允许被 `checked`。但是当父节点不是 `disabled` 时,它的子节点会被强制勾选。这样就会导致 `disabled` 节点“可以”被勾选,显然不合理。

### 所有的可勾选节点都被勾选

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*BzrZRbT1gCEAAAAAAAAAAAAADrJ8AQ/original)

从勾选交互看,它看起来不错,但是并不符合直觉。`parent 1` 勾选后,`leaf 2` 被传导勾选。但是其中间节点 `parent 1-0` 却没有勾选。在一些足够深的层级下,这种策略会导致用户并不知道勾选被传导出去了:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*3mHLQZvTgWsAAAAAAAAAAAAADrJ8AQ/original)

没有滚动时,用户并不能意识到上层 `disabled` 都没有被勾选时,而最上面被勾选了:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*xTqPQbdX6B0AAAAAAAAAAAAADrJ8AQ/original)

### 只勾选可触达的可勾选节点

这也是 antd 现在的策略,当节点被勾选时,它会从节点起向上向下传导,直到 `disabled` 停止。节点中存在多个 `disabled` 时会各自进行勾选状态管理:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*EIK0Rbq92CMAAAAAAAAAAAAADrJ8AQ/original)

反过来勾选 `leaf 2`,也不会传导:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*Ytr9SrJUvD4AAAAAAAAAAAAADrJ8AQ/original)

这种策略的好处是,用户可以清晰的看到勾选的传导过程。相对于上一个策略,在滚动场景用户只需要很小的区域就能理解勾选逻辑。

## 一些实现细节

注:我们此处只做简单的传导逻辑介绍,具体应用请参考 [实际代码](https://github.com/react-component/tree/blob/62e0bf0b91d86b6e42fee69870ada9a4640b6c6f/src/utils/conductUtil.ts)。其中还会做一些性能优化,比如通过缓存机制跳过已经被遍历过的节点。

### 勾选传导

当勾选节点后,我们会将 `key` 加入到 `checkedKeys` 中。我们会遍历新的 `checkedKeys` 中的每个 `key` 进行传导检查。第一步会自上而下进行传导(下图示例我们勾选 `0-0`):

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*30UnR60SSD8AAAAAAAAAAAAADrJ8AQ/original)

我们将当前节点 `0-0` 和被传导的 `0-0-0`与 `0-0-1` 记录下来:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*jo7wQZVX9S0AAAAAAAAAAAAADrJ8AQ/original)

第二步,我们会从该节点向上传导:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*k5hoSKM1OMYAAAAAAAAAAAAADrJ8AQ/original)

同样的,将被传导的节点 `0` 记录下来:

![Tree](https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*yqBETbq8ugQAAAAAAAAAAAAADrJ8AQ/original)

当父节点被传导勾选时,该父节点的父节点勾选也可能会被传导,所以我们需要继续向上传导,直到 根节点 或者 `disabled` 节点为止。

### 取消勾选传导

同上,我们一样会向上、向下进行传导遍历,然后将传导的节点从 `checkedKeys` 中移除。因此不再复述。

## 最后

在 v3 早期以前,我们遇到关于 Tree 的 `disabled` 勾选在不同的场景会有不同的诉求(而在零散的查看诉求时每个都“很合理”),而当抽离出来检视时,我们发现这些零散的诉求又会相互冲突。因而我们对其传导逻辑进行了一次整理,选择了最直观的一种策略。当然,如果当前的实现不满足需求时,你可以通过 `checkStrictly` 自行实现。