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); + }); + }); });