Skip to content

Commit 2e780c0

Browse files
authored
feat: Simple select ✨ (#60)
* feat: Simple select ✨ * fix: Attempt to fix CodeClimate issues * refactor: Move node-label to its own file (reduce code complexity) * chore: Remove console logs * docs: Example for simple select * feat: Select single node only with simpleSelect * refactor: Trying to remove cognitive complexity
1 parent 86d10f7 commit 2e780c0

File tree

15 files changed

+306
-58
lines changed

15 files changed

+306
-58
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
---
44

5-
[![NPM version][npm-image]][npm-url] [![gzip][gzip-image]][gzip-url] [![npm download][download-image]][npm-url]
5+
[![NPM version][npm-image]][npm-url] [![gzip][gzip-image]][gzip-url] [![npm download][download-image]][npm-url]
66

77
[![build status][travis-image]][travis-url] [![Test coverage][coveralls-image]][coveralls-url] [![semantic-release][semantic-release]][semantic-release-url] [![Commitizen friendly][commitizen]][commitizen-url] [![Greenkeeper badge][greenkeeper]][greenkeeper-url]
88

@@ -75,6 +75,10 @@ Online demo: http://dowjones.github.io/react-dropdown-tree-select/examples/boots
7575

7676
Online demo: http://dowjones.github.io/react-dropdown-tree-select/examples/material
7777

78+
##### As Single Select
79+
80+
Online demo: http://dowjones.github.io/react-dropdown-tree-select/examples/simple
81+
7882
## Install
7983

8084
```
@@ -223,7 +227,13 @@ The text to display as placeholder on the search box. Defaults to `Choose...`
223227

224228
Type: `bool`
225229

226-
Displays search results as a tree instead of flatten results
230+
Displays search results as a tree instead of flattened results
231+
232+
### simpleSelect
233+
234+
Type: `bool` (default: `false`)
235+
236+
Turns the dropdown into a simple, single select dropdown. If you pass tree data, only immediate children are picked, grandchildren nodes are ignored. Defaults to `false`.
227237

228238
## Styling and Customization
229239

docs/examples/simple/index.css

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
body, html, #app {
6+
width: 100%;
7+
height: 100%;
8+
margin: 0;
9+
padding: 0;
10+
font-family: monospace;
11+
}
12+
13+
body {
14+
margin: 20px;
15+
width: calc(100% - 10px);
16+
}
17+
18+
h2 {
19+
margin: 0;
20+
}
21+
22+
#app {
23+
height: 50%;
24+
width: 500px;
25+
}
26+
27+
.suspended .node-label {
28+
font-style: italic;
29+
text-decoration: line-through;
30+
}
31+
32+
.dropdown-content {
33+
max-height: 400px;
34+
overflow-y: auto;
35+
}
36+
37+
.node .fa {
38+
font-size: 12px;
39+
margin-left: 4px;
40+
cursor: pointer;
41+
}
42+
43+
.node .fa-ban {
44+
color: darkorange;
45+
}

docs/examples/simple/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>React Dropdown Tree Select Demo</title>
5+
<link rel="stylesheet" href="./index.css" />
6+
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">
7+
</head>
8+
<body>
9+
<h2>React Dropdown Tree Select Demo</h2>
10+
<br/>
11+
<div id="app"></div>
12+
<script src="bundle.js"></script>
13+
</body>
14+
</html>

docs/examples/simple/index.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom'
3+
import DropdownTreeSelect from '../../../src'
4+
import data from '../../demo-data.json'
5+
6+
const onChange = (curNode, selectedNodes) => {
7+
console.log('onChange::', curNode, selectedNodes)
8+
}
9+
const onAction = ({ action, node }) => {
10+
console.log(`onAction:: [${action}]`, node)
11+
}
12+
const onNodeToggle = curNode => {
13+
console.log('onNodeToggle::', curNode)
14+
}
15+
16+
ReactDOM.render(
17+
<DropdownTreeSelect
18+
data={data}
19+
onChange={onChange}
20+
onAction={onAction}
21+
onNodeToggle={onNodeToggle}
22+
keepTreeOnSearch
23+
simpleSelect
24+
/>,
25+
document.getElementById('app')
26+
)

docs/webpack.config.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,11 @@ module.exports = [{
6262
path: path.join(__dirname, 'examples/material'),
6363
filename: 'bundle.js'
6464
}
65+
}, {
66+
...baseConfig,
67+
entry: path.join(__dirname, 'examples/simple'),
68+
output: {
69+
path: path.join(__dirname, 'examples/simple'),
70+
filename: 'bundle.js'
71+
}
6572
}]

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"semantic-release": "semantic-release",
3434
"travis-deploy-once": "travis-deploy-once",
3535
"test": "cross-env NODE_ENV=test ava",
36-
"test:cov": "rimraf .nyc_output && nyc npm test"
36+
"test:cov": "rimraf .nyc_output && nyc npm test && nyc report --reporter=lcov "
3737
},
3838
"files": [
3939
"dist"

src/index.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class DropdownTreeSelect extends Component {
2525
className: PropTypes.string,
2626
onChange: PropTypes.func,
2727
onAction: PropTypes.func,
28-
onNodeToggle: PropTypes.func
28+
onNodeToggle: PropTypes.func,
29+
simpleSelect: PropTypes.bool
2930
}
3031

3132
constructor (props) {
@@ -40,8 +41,8 @@ class DropdownTreeSelect extends Component {
4041
typeof this.props.onChange === 'function' && this.props.onChange(...args)
4142
}
4243

43-
createList = tree => {
44-
this.treeManager = new TreeManager(tree)
44+
createList = (tree, simple) => {
45+
this.treeManager = new TreeManager(tree, simple)
4546
return this.treeManager.tree
4647
}
4748

@@ -57,13 +58,13 @@ class DropdownTreeSelect extends Component {
5758
}
5859

5960
componentWillMount () {
60-
const tree = this.createList(this.props.data)
61+
const tree = this.createList(this.props.data, this.props.simpleSelect)
6162
const tags = this.treeManager.getTags()
6263
this.setState({ tree, tags })
6364
}
6465

6566
componentWillReceiveProps (nextProps) {
66-
const tree = this.createList(nextProps.data)
67+
const tree = this.createList(nextProps.data, nextProps.simpleSelect)
6768
const tags = this.treeManager.getTags()
6869
this.setState({ tree, tags })
6970
}
@@ -116,7 +117,9 @@ class DropdownTreeSelect extends Component {
116117
onCheckboxChange = (id, checked) => {
117118
this.treeManager.setNodeCheckedState(id, checked)
118119
const tags = this.treeManager.getTags()
119-
this.setState({ tree: this.treeManager.tree, tags })
120+
const showDropdown = this.props.simpleSelect ? false : this.state.showDropdown
121+
this.setState({ tree: this.treeManager.tree, tags, showDropdown })
122+
if (this.props.simpleSelect) this.resetSearch()
120123
this.notifyChange(this.treeManager.getNodeById(id), tags)
121124
}
122125

@@ -132,6 +135,7 @@ class DropdownTreeSelect extends Component {
132135
top: this.state.showDropdown,
133136
bottom: !this.state.showDropdown
134137
})
138+
135139
return (
136140
<div
137141
className={cx(this.props.className, 'react-dropdown-tree-select')}
@@ -169,6 +173,7 @@ class DropdownTreeSelect extends Component {
169173
onAction={this.onAction}
170174
onCheckboxChange={this.onCheckboxChange}
171175
onNodeToggle={this.onNodeToggle}
176+
simpleSelect={this.props.simpleSelect}
172177
/>
173178
)}
174179
</div>

src/tree-manager/flatten-tree.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ const tree = [
9090
* @param {[type]} tree The incoming tree object
9191
* @return {object} The flattened list
9292
*/
93-
function flattenTree (tree) {
93+
function flattenTree (tree, simple) {
9494
const forest = Array.isArray(tree) ? tree : [tree]
95-
const list = walkNodes({nodes: forest})
95+
const list = walkNodes({ nodes: forest, simple })
9696
return list
9797
}
9898

@@ -114,7 +114,7 @@ function setInitialStateProps (node, parent = {}) {
114114
}
115115
}
116116

117-
function walkNodes ({nodes, list = new Map(), parent, depth = 0}) {
117+
function walkNodes ({ nodes, list = new Map(), parent, depth = 0, simple }) {
118118
nodes.forEach((node, i) => {
119119
node._depth = depth
120120

@@ -129,7 +129,7 @@ function walkNodes ({nodes, list = new Map(), parent, depth = 0}) {
129129
setInitialStateProps(node, parent)
130130

131131
list.set(node._id, node)
132-
if (node.children) {
132+
if (!simple && node.children) {
133133
node._children = []
134134
walkNodes({nodes: node.children, list, parent: node, depth: depth + 1})
135135
node.children = undefined

src/tree-manager/index.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import isEmpty from '../isEmpty'
22
import flattenTree from './flatten-tree'
33

44
class TreeManager {
5-
constructor (tree) {
5+
constructor (tree, simple) {
66
this._src = tree
7-
this.tree = flattenTree(JSON.parse(JSON.stringify(tree)))
7+
this.tree = flattenTree(JSON.parse(JSON.stringify(tree)), simple)
8+
this.simpleSelect = simple
89
this.searchMaps = new Map()
910
}
1011

@@ -82,13 +83,24 @@ class TreeManager {
8283
return this.tree
8384
}
8485

86+
togglePreviousChecked (id) {
87+
const prevChecked = this.currentChecked
88+
if (prevChecked) this.getNodeById(prevChecked).checked = false
89+
this.currentChecked = id
90+
}
91+
8592
setNodeCheckedState (id, checked) {
8693
const node = this.getNodeById(id)
8794
node.checked = checked
88-
this.toggleChildren(id, checked)
8995

90-
if (!checked) {
91-
this.unCheckParents(node)
96+
if (this.simpleSelect) {
97+
this.togglePreviousChecked(id)
98+
} else {
99+
this.toggleChildren(id, checked)
100+
101+
if (!checked) {
102+
this.unCheckParents(node)
103+
}
92104
}
93105
}
94106

src/tree-manager/index.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,3 +398,36 @@ test('should get matching nodes with mixed case when searched', t => {
398398
const nodes = ['i1', 'c1']
399399
nodes.forEach(n => t.false(manager.getNodeById(n).hide))
400400
})
401+
402+
test('should uncheck previous node in simple select mode', t => {
403+
const tree = [{
404+
id: 'i1',
405+
label: 'l1',
406+
value: 'v1',
407+
children: [{
408+
id: 'c1',
409+
label: 'l1c1',
410+
value: 'l1v1'
411+
}]
412+
}, {
413+
id: 'i2',
414+
label: 'l2',
415+
value: 'v2',
416+
children: [{
417+
id: 'c2',
418+
label: 'l2c2',
419+
value: 'l2v2'
420+
}]
421+
}]
422+
const manager = new TreeManager(tree, true)
423+
manager.setNodeCheckedState('i1', true)
424+
t.true(manager.getNodeById('i1').checked)
425+
426+
manager.setNodeCheckedState('i2', true)
427+
t.false(manager.getNodeById('i1').checked)
428+
t.true(manager.getNodeById('i2').checked)
429+
430+
manager.setNodeCheckedState('i1', true)
431+
t.true(manager.getNodeById('i1').checked)
432+
t.false(manager.getNodeById('i2').checked)
433+
})

0 commit comments

Comments
 (0)