Skip to content

Commit fd7e888

Browse files
committed
Add checkModel property to specify which nodes should be stored in the checked array
Currently only supporting 'leaf' (the default behavior) and 'all'. This lines up with models 1 and 3 on #13. Model 2 still needs implementation, although some pieces have been included in this commit.
1 parent 7ac28aa commit fd7e888

File tree

6 files changed

+182
-9
lines changed

6 files changed

+182
-9
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# CHANGELOG
22

3+
## v1.6.0 (TBA)
4+
5+
### New Features
6+
7+
* [#13]: Add `checkModel` property to specify which nodes should be stored in the `checked` array
8+
39
## [v1.5.0](https://github.com/jakezatecky/react-checkbox-tree/compare/v1.4.1...v1.5.0) (2019-01-25)
410

511
### New Features

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
145145
| -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | ----------- |
146146
| `nodes` | array | **Required**. Specifies the tree nodes and their children. | |
147147
| `checked` | array | An array of checked node values. | `[]` |
148+
| `checkModel` | bool | Specifies which nodes should be stored in the `checked` array. Accepts `'leaf'` or `'all'`. | `'leaf'` |
148149
| `disabled` | bool | If true, the component will be disabled and nodes cannot be checked. | `false` |
149150
| `expandDisabled` | bool | If true, the ability to expand nodes will be disabled. | `false` |
150151
| `expandOnClick` | bool | If true, nodes will be expanded by clicking on labels. Requires a non-empty `onClick` function. | `false` |

src/js/CheckboxTree.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
55
import React from 'react';
66

77
import Button from './Button';
8+
import constants from './constants';
89
import NodeModel from './NodeModel';
910
import TreeNode from './TreeNode';
1011
import iconsShape from './shapes/iconsShape';
@@ -16,6 +17,7 @@ class CheckboxTree extends React.Component {
1617
static propTypes = {
1718
nodes: PropTypes.arrayOf(nodeShape).isRequired,
1819

20+
checkModel: PropTypes.oneOf([constants.CheckModel.LEAF, constants.CheckModel.ALL]),
1921
checked: listShape,
2022
disabled: PropTypes.bool,
2123
expandDisabled: PropTypes.bool,
@@ -40,6 +42,7 @@ class CheckboxTree extends React.Component {
4042
};
4143

4244
static defaultProps = {
45+
checkModel: constants.CheckModel.LEAF,
4346
checked: [],
4447
disabled: false,
4548
expandDisabled: false,
@@ -127,11 +130,11 @@ class CheckboxTree extends React.Component {
127130
}
128131

129132
onCheck(nodeInfo) {
130-
const { noCascade, onCheck } = this.props;
133+
const { checkModel, noCascade, onCheck } = this.props;
131134
const model = this.state.model.clone();
132135
const node = model.getNode(nodeInfo.value);
133136

134-
model.toggleChecked(nodeInfo, nodeInfo.checked, noCascade);
137+
model.toggleChecked(nodeInfo, nodeInfo.checked, checkModel, noCascade);
135138
onCheck(model.serializeList('checked'), { ...node, ...nodeInfo });
136139
}
137140

src/js/NodeModel.js

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import constants from './constants';
2+
3+
const { CheckModel } = constants;
4+
15
class NodeModel {
26
constructor(props, nodes = {}) {
37
this.props = props;
@@ -40,6 +44,7 @@ class NodeModel {
4044
value: node.value,
4145
children: node.children,
4246
parent,
47+
isChild: parent.value !== undefined,
4348
isParent,
4449
isLeaf: !isParent,
4550
showCheckbox: node.showCheckbox !== undefined ? node.showCheckbox : true,
@@ -109,26 +114,58 @@ class NodeModel {
109114
return this;
110115
}
111116

112-
toggleChecked(node, isChecked, noCascade) {
117+
toggleChecked(node, isChecked, checkModel, noCascade, percolateUpward = true) {
113118
const flatNode = this.flatNodes[node.value];
119+
const modelHasParents = [CheckModel.PARENT, CheckModel.ALL].indexOf(checkModel) > -1;
120+
const modelHasLeaves = [CheckModel.LEAF, CheckModel.ALL].indexOf(checkModel) > -1;
114121

115122
if (flatNode.isLeaf || noCascade) {
116123
if (node.disabled) {
117124
return this;
118125
}
119126

120-
// Set the check status of a leaf node or an uncoupled parent
121127
this.toggleNode(node.value, 'checked', isChecked);
122128
} else {
123-
// Percolate check status down to all children
124-
flatNode.children.forEach((child) => {
125-
this.toggleChecked(child, isChecked, noCascade);
126-
});
129+
if (modelHasParents) {
130+
this.toggleNode(node.value, 'checked', isChecked);
131+
}
132+
133+
if (modelHasLeaves) {
134+
// Percolate check status down to all children
135+
flatNode.children.forEach((child) => {
136+
this.toggleChecked(child, isChecked, checkModel, noCascade, false);
137+
});
138+
}
139+
}
140+
141+
// Percolate check status up to parent
142+
// The check model must include parent nodes and we must not have already covered the
143+
// parent (relevant only when percolating through children)
144+
if (percolateUpward && !noCascade && flatNode.isChild && modelHasParents) {
145+
this.toggleParentStatus(flatNode.parent, checkModel);
127146
}
128147

129148
return this;
130149
}
131150

151+
toggleParentStatus(node, checkModel) {
152+
const flatNode = this.flatNodes[node.value];
153+
154+
if (flatNode.isChild) {
155+
if (checkModel === CheckModel.ALL) {
156+
this.toggleNode(node.value, 'checked', this.isEveryChildChecked(flatNode));
157+
}
158+
159+
this.toggleParentStatus(flatNode.parent, checkModel);
160+
} else {
161+
this.toggleNode(node.value, 'checked', this.isEveryChildChecked(flatNode));
162+
}
163+
}
164+
165+
isEveryChildChecked(node) {
166+
return node.children.every(child => this.getNode(child.value).checked);
167+
}
168+
132169
toggleNode(nodeValue, key, toggleValue) {
133170
this.flatNodes[nodeValue][key] = toggleValue;
134171

src/js/constants.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const CheckModel = {
2+
ALL: 'all',
3+
PARENT: 'parent',
4+
LEAF: 'leaf',
5+
};
6+
7+
export default { CheckModel };

test/CheckboxTree.js

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,125 @@ describe('<CheckboxTree />', () => {
2020
});
2121
});
2222

23+
describe('checkModel', () => {
24+
describe('all', () => {
25+
it('should record checked parent and leaf nodes', () => {
26+
let actual = null;
27+
28+
const wrapper = mount(
29+
<CheckboxTree
30+
checkModel="all"
31+
nodes={[
32+
{
33+
value: 'jupiter',
34+
label: 'Jupiter',
35+
children: [
36+
{ value: 'io', label: 'Io' },
37+
{ value: 'europa', label: 'Europa' },
38+
],
39+
},
40+
]}
41+
onCheck={(checked) => {
42+
actual = checked;
43+
}}
44+
/>,
45+
);
46+
47+
wrapper.find('TreeNode input[type="checkbox"]').simulate('click');
48+
assert.deepEqual(['jupiter', 'io', 'europa'], actual);
49+
});
50+
51+
it('should percolate `checked` to all parents and grandparents if all leaves are checked', () => {
52+
let actual = null;
53+
54+
const wrapper = mount(
55+
<CheckboxTree
56+
checkModel="all"
57+
checked={['mercury', 'io']}
58+
expanded={['sol', 'jupiter']}
59+
nodes={[
60+
{
61+
value: 'sol',
62+
label: 'Sol System',
63+
children: [
64+
{ value: 'mercury', label: 'Mercury' },
65+
{
66+
value: 'jupiter',
67+
label: 'Jupiter',
68+
children: [
69+
{ value: 'io', label: 'Io' },
70+
{ value: 'europa', label: 'Europa' },
71+
],
72+
},
73+
],
74+
},
75+
]}
76+
onCheck={(checked) => {
77+
actual = checked;
78+
}}
79+
/>,
80+
);
81+
82+
wrapper.find('TreeNode[value="europa"] input[type="checkbox"]').simulate('click');
83+
assert.deepEqual(['sol', 'mercury', 'jupiter', 'io', 'europa'], actual);
84+
});
85+
86+
it('should NOT percolate `checked` to the parent if not all leaves are checked', () => {
87+
let actual = null;
88+
89+
const wrapper = mount(
90+
<CheckboxTree
91+
checkModel="all"
92+
expanded={['jupiter']}
93+
nodes={[
94+
{
95+
value: 'jupiter',
96+
label: 'Jupiter',
97+
children: [
98+
{ value: 'io', label: 'Io' },
99+
{ value: 'europa', label: 'Europa' },
100+
],
101+
},
102+
]}
103+
onCheck={(checked) => {
104+
actual = checked;
105+
}}
106+
/>,
107+
);
108+
109+
wrapper.find('TreeNode[value="europa"] input[type="checkbox"]').simulate('click');
110+
assert.deepEqual(['europa'], actual);
111+
});
112+
});
113+
114+
describe('leaf', () => {
115+
it('should only record leaf nodes in the checked array', () => {
116+
let actual = null;
117+
118+
const wrapper = mount(
119+
<CheckboxTree
120+
nodes={[
121+
{
122+
value: 'jupiter',
123+
label: 'Jupiter',
124+
children: [
125+
{ value: 'io', label: 'Io' },
126+
{ value: 'europa', label: 'Europa' },
127+
],
128+
},
129+
]}
130+
onCheck={(checked) => {
131+
actual = checked;
132+
}}
133+
/>,
134+
);
135+
136+
wrapper.find('TreeNode input[type="checkbox"]').simulate('click');
137+
assert.deepEqual(['io', 'europa'], actual);
138+
});
139+
});
140+
});
141+
23142
describe('checked', () => {
24143
it('should not throw an exception if it contains values that are not in the `nodes` property', () => {
25144
const wrapper = shallow(
@@ -337,7 +456,7 @@ describe('<CheckboxTree />', () => {
337456
});
338457

339458
describe('onlyLeafCheckboxes', () => {
340-
it('should only render show checkboxes for leaf nodes', () => {
459+
it('should only render checkboxes for leaf nodes', () => {
341460
const wrapper = mount(
342461
<CheckboxTree
343462
expanded={['jupiter']}

0 commit comments

Comments
 (0)