diff --git a/CHANGELOG.md b/CHANGELOG.md
index d666995c..aea0a89e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# CHANGELOG
+## v0.6.2 (TBD)
+
+### New Features
+
+* [#34]: Add `noCascade` option to decouple parent check state from children
+
## [v0.6.1](https://github.com/jakezatecky/react-checkbox-tree/compare/v0.6.0...v0.6.1) (2017-05-09)
### Other
diff --git a/README.md b/README.md
index 1d5dcf5c..14b98f86 100644
--- a/README.md
+++ b/README.md
@@ -96,6 +96,7 @@ All node objects **must** have a unique `value`. This value is serialized into t
| `expanded` | array | An array of expanded node values. | `[]` |
| `name` | string | Optional name for the hidden ` ` element. | `undefined` |
| `nameAsArray` | bool | If true, the hidden ` ` will encode its values as an array rather than a joined string. | `false` |
+| `noCascade` | bool | If true, the toggling a parent will **not** cascade its check state to its children. | `false` |
| `optimisticToggle` | bool | If true, toggling a partially-checked node will select all children. If false, it will deselect. | `true` |
| `showNodeIcon` | bool | If true, each node will show a parent or leaf icon. | `true` |
| `onCheck` | function | onCheck handler: `function(checked) {}` | `() => {}` |
diff --git a/examples/src/index.html b/examples/src/index.html
index bc901bd5..f4dd9497 100644
--- a/examples/src/index.html
+++ b/examples/src/index.html
@@ -28,8 +28,19 @@
Basic Example
Custom Icons Example
+ No Cascading Example
+
+ By default, the check state of a parent is determined by the check state of its children. Similarly, checking or
+ unchecking a parent node will cascade that status to all of its children. To disable this behavior, simply pass
+ the noCascade
property.
+
+
+
Pessimistic Toggle Example
- Try clicking a partially-checked node. Instead of select all children, the pessimistic model will uncheck them.
+
+ Try clicking a partially-checked node below. Instead of cascading a checked state to all children, the
+ pessimistic model will uncheck children and their descendents.
+
Large Data Example
diff --git a/examples/src/index.js b/examples/src/index.js
index a1a27897..fc0f0084 100644
--- a/examples/src/index.js
+++ b/examples/src/index.js
@@ -3,10 +3,12 @@ import ReactDOM from 'react-dom';
import BasicExample from './js/BasicExample';
import CustomIconsExample from './js/CustomIconsExample';
+import NoCascadeExample from './js/NoCascadeExample';
import PessimisticToggleExample from './js/PessimisticToggleExample';
import LargeDataExample from './js/LargeDataExample';
ReactDOM.render( , document.getElementById('basic-example'));
ReactDOM.render( , document.getElementById('custom-icons-example'));
+ReactDOM.render( , document.getElementById('no-cascade-example'));
ReactDOM.render( , document.getElementById('pessimistic-toggle-example'));
ReactDOM.render( , document.getElementById('large-data-example'));
diff --git a/examples/src/js/NoCascadeExample.js b/examples/src/js/NoCascadeExample.js
new file mode 100644
index 00000000..cf1199ea
--- /dev/null
+++ b/examples/src/js/NoCascadeExample.js
@@ -0,0 +1,130 @@
+import React from 'react';
+import CheckboxTree from 'react-checkbox-tree';
+
+const nodes = [
+ {
+ value: '/app',
+ label: 'app',
+ children: [
+ {
+ value: '/app/Http',
+ label: 'Http',
+ children: [
+ {
+ value: '/app/Http/Controllers',
+ label: 'Controllers',
+ children: [{
+ value: '/app/Http/Controllers/WelcomeController.js',
+ label: 'WelcomeController.js',
+ }],
+ },
+ {
+ value: '/app/Http/routes.js',
+ label: 'routes.js',
+ },
+ ],
+ },
+ {
+ value: '/app/Providers',
+ label: 'Providers',
+ children: [{
+ value: '/app/Http/Providers/EventServiceProvider.js',
+ label: 'EventServiceProvider.js',
+ }],
+ },
+ ],
+ },
+ {
+ value: '/config',
+ label: 'config',
+ children: [
+ {
+ value: '/config/app.js',
+ label: 'app.js',
+ },
+ {
+ value: '/config/database.js',
+ label: 'database.js',
+ },
+ ],
+ },
+ {
+ value: '/public',
+ label: 'public',
+ children: [
+ {
+ value: '/public/assets/',
+ label: 'assets',
+ children: [{
+ value: '/public/assets/style.css',
+ label: 'style.css',
+ }],
+ },
+ {
+ value: '/public/index.html',
+ label: 'index.html',
+ },
+ ],
+ },
+ {
+ value: '/.env',
+ label: '.env',
+ },
+ {
+ value: '/.gitignore',
+ label: '.gitignore',
+ },
+ {
+ value: '/README.md',
+ label: 'README.md',
+ },
+];
+
+class NoCascadeExample extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ checked: [
+ '/app/Http/Controllers/WelcomeController.js',
+ '/app/Http/routes.js',
+ '/public/assets/style.css',
+ '/public/index.html',
+ '/.gitignore',
+ ],
+ expanded: [
+ '/app',
+ '/app/Http',
+ ],
+ cascadeToggle: 'optimistic',
+ };
+
+ this.onCheck = this.onCheck.bind(this);
+ this.onExpand = this.onExpand.bind(this);
+ }
+
+ onCheck(checked) {
+ this.setState({ checked });
+ }
+
+ onExpand(expanded) {
+ this.setState({ expanded });
+ }
+
+ render() {
+ const { checked, expanded } = this.state;
+
+ return (
+
+ );
+ }
+}
+
+export default NoCascadeExample;
diff --git a/src/js/CheckboxTree.js b/src/js/CheckboxTree.js
index 8a5e7a7a..01efc7d0 100644
--- a/src/js/CheckboxTree.js
+++ b/src/js/CheckboxTree.js
@@ -14,6 +14,7 @@ class CheckboxTree extends React.Component {
expanded: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.string,
nameAsArray: PropTypes.bool,
+ noCascade: PropTypes.bool,
optimisticToggle: PropTypes.bool,
showNodeIcon: PropTypes.bool,
onCheck: PropTypes.func,
@@ -25,6 +26,7 @@ class CheckboxTree extends React.Component {
expanded: [],
name: undefined,
nameAsArray: false,
+ noCascade: false,
optimisticToggle: true,
showNodeIcon: true,
onCheck: () => {},
@@ -56,9 +58,9 @@ class CheckboxTree extends React.Component {
}
onCheck(node) {
- const { onCheck } = this.props;
+ const { noCascade, onCheck } = this.props;
- this.toggleChecked(node, node.checked);
+ this.toggleChecked(node, node.checked, noCascade);
onCheck(this.serializeList('checked'));
}
@@ -86,8 +88,8 @@ class CheckboxTree extends React.Component {
});
}
- getCheckState(node) {
- if (node.children === null) {
+ getCheckState(node, noCascade) {
+ if (node.children === null || noCascade) {
return node.checked ? 1 : 0;
}
@@ -102,15 +104,15 @@ class CheckboxTree extends React.Component {
return 0;
}
- toggleChecked(node, isChecked) {
- if (node.children !== null) {
+ toggleChecked(node, isChecked, noCascade) {
+ if (node.children === null || noCascade) {
+ // Set the check status of a leaf node or an uncoupled parent
+ this.toggleNode('checked', node, isChecked);
+ } else {
// Percolate check status down to all children
node.children.forEach((child) => {
this.toggleChecked(child, isChecked);
});
- } else {
- // Set leaf to check/unchecked state
- this.toggleNode('checked', node, isChecked);
}
}
@@ -178,9 +180,10 @@ class CheckboxTree extends React.Component {
}
renderTreeNodes(nodes) {
+ const { noCascade, optimisticToggle, showNodeIcon } = this.props;
const treeNodes = nodes.map((node) => {
const key = `${node.value}`;
- const checked = this.getCheckState(node);
+ const checked = this.getCheckState(node, noCascade);
const children = this.renderChildNodes(node);
return (
@@ -191,9 +194,9 @@ class CheckboxTree extends React.Component {
expanded={node.expanded}
icon={node.icon}
label={node.label}
- optimisticToggle={this.props.optimisticToggle}
+ optimisticToggle={optimisticToggle}
rawChildren={node.children}
- showNodeIcon={this.props.showNodeIcon}
+ showNodeIcon={showNodeIcon}
treeId={this.id}
value={node.value}
onCheck={this.onCheck}
diff --git a/src/js/TreeNode.js b/src/js/TreeNode.js
index bf4f16b3..d21df0b5 100644
--- a/src/js/TreeNode.js
+++ b/src/js/TreeNode.js
@@ -44,7 +44,7 @@ class TreeNode extends React.Component {
isChecked = true;
}
- // Toggle partial state based on model
+ // Toggle partial state based on cascade model
if (this.props.checked === 2) {
isChecked = this.props.optimisticToggle;
}
diff --git a/test/CheckboxTree.js b/test/CheckboxTree.js
index 2c39387f..d511a7c0 100644
--- a/test/CheckboxTree.js
+++ b/test/CheckboxTree.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
import { assert } from 'chai';
import CheckboxTree from '../src/js/CheckboxTree';
@@ -77,5 +77,60 @@ describe(' ', () => {
assert.deepEqual({ value: 'europa', label: 'Europa' }, { value, label });
});
});
+
+ describe('noCascade', () => {
+ it('should not toggle the check state of children when set to true', () => {
+ let actual = null;
+
+ const wrapper = mount(
+ {
+ actual = checked;
+ }}
+ />,
+ );
+
+ wrapper.find('TreeNode input[type="checkbox"]').simulate('change');
+ assert.deepEqual(['jupiter'], actual);
+ });
+
+ it('should toggle the check state of children when set to false', () => {
+ let actual = null;
+
+ const wrapper = mount(
+ {
+ actual = checked;
+ }}
+ />,
+ );
+
+ wrapper.find('TreeNode input[type="checkbox"]').simulate('change');
+ assert.deepEqual(['io', 'europa'], actual);
+ });
+ });
});