Skip to content

Commit

Permalink
Add noCascade option
Browse files Browse the repository at this point in the history
Resolves #34
  • Loading branch information
jakezatecky committed May 25, 2017
1 parent 34ca7c8 commit 87fe44f
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 15 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<input>` element. | `undefined` |
| `nameAsArray` | bool | If true, the hidden `<input>` 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) {}` | `() => {}` |
Expand Down
13 changes: 12 additions & 1 deletion examples/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,19 @@ <h2>Basic Example</h2>
<h2>Custom Icons Example</h2>
<div id="custom-icons-example"></div>

<h2>No Cascading Example</h2>
<p>
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 <code>noCascade</code> property.
</p>
<div id="no-cascade-example"></div>

<h2>Pessimistic Toggle Example</h2>
<p>Try clicking a partially-checked node. Instead of select all children, the pessimistic model will uncheck them.</p>
<p>
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.
</p>
<div id="pessimistic-toggle-example"></div>

<h2>Large Data Example</h2>
Expand Down
2 changes: 2 additions & 0 deletions examples/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(<BasicExample />, document.getElementById('basic-example'));
ReactDOM.render(<CustomIconsExample />, document.getElementById('custom-icons-example'));
ReactDOM.render(<NoCascadeExample />, document.getElementById('no-cascade-example'));
ReactDOM.render(<PessimisticToggleExample />, document.getElementById('pessimistic-toggle-example'));
ReactDOM.render(<LargeDataExample />, document.getElementById('large-data-example'));
130 changes: 130 additions & 0 deletions examples/src/js/NoCascadeExample.js
Original file line number Diff line number Diff line change
@@ -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 (
<CheckboxTree
checked={checked}
expanded={expanded}
noCascade
nodes={nodes}
onCheck={this.onCheck}
onExpand={this.onExpand}
/>
);
}
}

export default NoCascadeExample;
27 changes: 15 additions & 12 deletions src/js/CheckboxTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,6 +26,7 @@ class CheckboxTree extends React.Component {
expanded: [],
name: undefined,
nameAsArray: false,
noCascade: false,
optimisticToggle: true,
showNodeIcon: true,
onCheck: () => {},
Expand Down Expand Up @@ -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'));
}

Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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 (
Expand All @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion src/js/TreeNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
57 changes: 56 additions & 1 deletion test/CheckboxTree.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -77,5 +77,60 @@ describe('<CheckboxTree />', () => {
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(
<CheckboxTree
checked={[]}
noCascade
nodes={[
{
value: 'jupiter',
label: 'Jupiter',
children: [
{ value: 'io', label: 'Io' },
{ value: 'europa', label: 'Europa' },
],
},
]}
onCheck={(checked) => {
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(
<CheckboxTree
checked={[]}
nodes={[
{
value: 'jupiter',
label: 'Jupiter',
children: [
{ value: 'io', label: 'Io' },
{ value: 'europa', label: 'Europa' },
],
},
]}
onCheck={(checked) => {
actual = checked;
}}
/>,
);

wrapper.find('TreeNode input[type="checkbox"]').simulate('change');
assert.deepEqual(['io', 'europa'], actual);
});
});
});

0 comments on commit 87fe44f

Please sign in to comment.