Skip to content

Commit 87fe44f

Browse files
committed
Add noCascade option
Resolves #34
1 parent 34ca7c8 commit 87fe44f

File tree

8 files changed

+223
-15
lines changed

8 files changed

+223
-15
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+
## v0.6.2 (TBD)
4+
5+
### New Features
6+
7+
* [#34]: Add `noCascade` option to decouple parent check state from children
8+
39
## [v0.6.1](https://github.com/jakezatecky/react-checkbox-tree/compare/v0.6.0...v0.6.1) (2017-05-09)
410

511
### Other

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ All node objects **must** have a unique `value`. This value is serialized into t
9696
| `expanded` | array | An array of expanded node values. | `[]` |
9797
| `name` | string | Optional name for the hidden `<input>` element. | `undefined` |
9898
| `nameAsArray` | bool | If true, the hidden `<input>` will encode its values as an array rather than a joined string. | `false` |
99+
| `noCascade` | bool | If true, the toggling a parent will **not** cascade its check state to its children. | `false` |
99100
| `optimisticToggle` | bool | If true, toggling a partially-checked node will select all children. If false, it will deselect. | `true` |
100101
| `showNodeIcon` | bool | If true, each node will show a parent or leaf icon. | `true` |
101102
| `onCheck` | function | onCheck handler: `function(checked) {}` | `() => {}` |

examples/src/index.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,19 @@ <h2>Basic Example</h2>
2828
<h2>Custom Icons Example</h2>
2929
<div id="custom-icons-example"></div>
3030

31+
<h2>No Cascading Example</h2>
32+
<p>
33+
By default, the check state of a parent is determined by the check state of its children. Similarly, checking or
34+
unchecking a parent node will cascade that status to all of its children. To disable this behavior, simply pass
35+
the <code>noCascade</code> property.
36+
</p>
37+
<div id="no-cascade-example"></div>
38+
3139
<h2>Pessimistic Toggle Example</h2>
32-
<p>Try clicking a partially-checked node. Instead of select all children, the pessimistic model will uncheck them.</p>
40+
<p>
41+
Try clicking a partially-checked node below. Instead of cascading a checked state to all children, the
42+
pessimistic model will uncheck children and their descendents.
43+
</p>
3344
<div id="pessimistic-toggle-example"></div>
3445

3546
<h2>Large Data Example</h2>

examples/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import ReactDOM from 'react-dom';
33

44
import BasicExample from './js/BasicExample';
55
import CustomIconsExample from './js/CustomIconsExample';
6+
import NoCascadeExample from './js/NoCascadeExample';
67
import PessimisticToggleExample from './js/PessimisticToggleExample';
78
import LargeDataExample from './js/LargeDataExample';
89

910
ReactDOM.render(<BasicExample />, document.getElementById('basic-example'));
1011
ReactDOM.render(<CustomIconsExample />, document.getElementById('custom-icons-example'));
12+
ReactDOM.render(<NoCascadeExample />, document.getElementById('no-cascade-example'));
1113
ReactDOM.render(<PessimisticToggleExample />, document.getElementById('pessimistic-toggle-example'));
1214
ReactDOM.render(<LargeDataExample />, document.getElementById('large-data-example'));

examples/src/js/NoCascadeExample.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import React from 'react';
2+
import CheckboxTree from 'react-checkbox-tree';
3+
4+
const nodes = [
5+
{
6+
value: '/app',
7+
label: 'app',
8+
children: [
9+
{
10+
value: '/app/Http',
11+
label: 'Http',
12+
children: [
13+
{
14+
value: '/app/Http/Controllers',
15+
label: 'Controllers',
16+
children: [{
17+
value: '/app/Http/Controllers/WelcomeController.js',
18+
label: 'WelcomeController.js',
19+
}],
20+
},
21+
{
22+
value: '/app/Http/routes.js',
23+
label: 'routes.js',
24+
},
25+
],
26+
},
27+
{
28+
value: '/app/Providers',
29+
label: 'Providers',
30+
children: [{
31+
value: '/app/Http/Providers/EventServiceProvider.js',
32+
label: 'EventServiceProvider.js',
33+
}],
34+
},
35+
],
36+
},
37+
{
38+
value: '/config',
39+
label: 'config',
40+
children: [
41+
{
42+
value: '/config/app.js',
43+
label: 'app.js',
44+
},
45+
{
46+
value: '/config/database.js',
47+
label: 'database.js',
48+
},
49+
],
50+
},
51+
{
52+
value: '/public',
53+
label: 'public',
54+
children: [
55+
{
56+
value: '/public/assets/',
57+
label: 'assets',
58+
children: [{
59+
value: '/public/assets/style.css',
60+
label: 'style.css',
61+
}],
62+
},
63+
{
64+
value: '/public/index.html',
65+
label: 'index.html',
66+
},
67+
],
68+
},
69+
{
70+
value: '/.env',
71+
label: '.env',
72+
},
73+
{
74+
value: '/.gitignore',
75+
label: '.gitignore',
76+
},
77+
{
78+
value: '/README.md',
79+
label: 'README.md',
80+
},
81+
];
82+
83+
class NoCascadeExample extends React.Component {
84+
constructor() {
85+
super();
86+
87+
this.state = {
88+
checked: [
89+
'/app/Http/Controllers/WelcomeController.js',
90+
'/app/Http/routes.js',
91+
'/public/assets/style.css',
92+
'/public/index.html',
93+
'/.gitignore',
94+
],
95+
expanded: [
96+
'/app',
97+
'/app/Http',
98+
],
99+
cascadeToggle: 'optimistic',
100+
};
101+
102+
this.onCheck = this.onCheck.bind(this);
103+
this.onExpand = this.onExpand.bind(this);
104+
}
105+
106+
onCheck(checked) {
107+
this.setState({ checked });
108+
}
109+
110+
onExpand(expanded) {
111+
this.setState({ expanded });
112+
}
113+
114+
render() {
115+
const { checked, expanded } = this.state;
116+
117+
return (
118+
<CheckboxTree
119+
checked={checked}
120+
expanded={expanded}
121+
noCascade
122+
nodes={nodes}
123+
onCheck={this.onCheck}
124+
onExpand={this.onExpand}
125+
/>
126+
);
127+
}
128+
}
129+
130+
export default NoCascadeExample;

src/js/CheckboxTree.js

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class CheckboxTree extends React.Component {
1414
expanded: PropTypes.arrayOf(PropTypes.string),
1515
name: PropTypes.string,
1616
nameAsArray: PropTypes.bool,
17+
noCascade: PropTypes.bool,
1718
optimisticToggle: PropTypes.bool,
1819
showNodeIcon: PropTypes.bool,
1920
onCheck: PropTypes.func,
@@ -25,6 +26,7 @@ class CheckboxTree extends React.Component {
2526
expanded: [],
2627
name: undefined,
2728
nameAsArray: false,
29+
noCascade: false,
2830
optimisticToggle: true,
2931
showNodeIcon: true,
3032
onCheck: () => {},
@@ -56,9 +58,9 @@ class CheckboxTree extends React.Component {
5658
}
5759

5860
onCheck(node) {
59-
const { onCheck } = this.props;
61+
const { noCascade, onCheck } = this.props;
6062

61-
this.toggleChecked(node, node.checked);
63+
this.toggleChecked(node, node.checked, noCascade);
6264
onCheck(this.serializeList('checked'));
6365
}
6466

@@ -86,8 +88,8 @@ class CheckboxTree extends React.Component {
8688
});
8789
}
8890

89-
getCheckState(node) {
90-
if (node.children === null) {
91+
getCheckState(node, noCascade) {
92+
if (node.children === null || noCascade) {
9193
return node.checked ? 1 : 0;
9294
}
9395

@@ -102,15 +104,15 @@ class CheckboxTree extends React.Component {
102104
return 0;
103105
}
104106

105-
toggleChecked(node, isChecked) {
106-
if (node.children !== null) {
107+
toggleChecked(node, isChecked, noCascade) {
108+
if (node.children === null || noCascade) {
109+
// Set the check status of a leaf node or an uncoupled parent
110+
this.toggleNode('checked', node, isChecked);
111+
} else {
107112
// Percolate check status down to all children
108113
node.children.forEach((child) => {
109114
this.toggleChecked(child, isChecked);
110115
});
111-
} else {
112-
// Set leaf to check/unchecked state
113-
this.toggleNode('checked', node, isChecked);
114116
}
115117
}
116118

@@ -178,9 +180,10 @@ class CheckboxTree extends React.Component {
178180
}
179181

180182
renderTreeNodes(nodes) {
183+
const { noCascade, optimisticToggle, showNodeIcon } = this.props;
181184
const treeNodes = nodes.map((node) => {
182185
const key = `${node.value}`;
183-
const checked = this.getCheckState(node);
186+
const checked = this.getCheckState(node, noCascade);
184187
const children = this.renderChildNodes(node);
185188

186189
return (
@@ -191,9 +194,9 @@ class CheckboxTree extends React.Component {
191194
expanded={node.expanded}
192195
icon={node.icon}
193196
label={node.label}
194-
optimisticToggle={this.props.optimisticToggle}
197+
optimisticToggle={optimisticToggle}
195198
rawChildren={node.children}
196-
showNodeIcon={this.props.showNodeIcon}
199+
showNodeIcon={showNodeIcon}
197200
treeId={this.id}
198201
value={node.value}
199202
onCheck={this.onCheck}

src/js/TreeNode.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class TreeNode extends React.Component {
4444
isChecked = true;
4545
}
4646

47-
// Toggle partial state based on model
47+
// Toggle partial state based on cascade model
4848
if (this.props.checked === 2) {
4949
isChecked = this.props.optimisticToggle;
5050
}

test/CheckboxTree.js

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { shallow } from 'enzyme';
2+
import { shallow, mount } from 'enzyme';
33
import { assert } from 'chai';
44

55
import CheckboxTree from '../src/js/CheckboxTree';
@@ -77,5 +77,60 @@ describe('<CheckboxTree />', () => {
7777
assert.deepEqual({ value: 'europa', label: 'Europa' }, { value, label });
7878
});
7979
});
80+
81+
describe('noCascade', () => {
82+
it('should not toggle the check state of children when set to true', () => {
83+
let actual = null;
84+
85+
const wrapper = mount(
86+
<CheckboxTree
87+
checked={[]}
88+
noCascade
89+
nodes={[
90+
{
91+
value: 'jupiter',
92+
label: 'Jupiter',
93+
children: [
94+
{ value: 'io', label: 'Io' },
95+
{ value: 'europa', label: 'Europa' },
96+
],
97+
},
98+
]}
99+
onCheck={(checked) => {
100+
actual = checked;
101+
}}
102+
/>,
103+
);
104+
105+
wrapper.find('TreeNode input[type="checkbox"]').simulate('change');
106+
assert.deepEqual(['jupiter'], actual);
107+
});
108+
109+
it('should toggle the check state of children when set to false', () => {
110+
let actual = null;
111+
112+
const wrapper = mount(
113+
<CheckboxTree
114+
checked={[]}
115+
nodes={[
116+
{
117+
value: 'jupiter',
118+
label: 'Jupiter',
119+
children: [
120+
{ value: 'io', label: 'Io' },
121+
{ value: 'europa', label: 'Europa' },
122+
],
123+
},
124+
]}
125+
onCheck={(checked) => {
126+
actual = checked;
127+
}}
128+
/>,
129+
);
130+
131+
wrapper.find('TreeNode input[type="checkbox"]').simulate('change');
132+
assert.deepEqual(['io', 'europa'], actual);
133+
});
134+
});
80135
});
81136

0 commit comments

Comments
 (0)