├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── examples ├── simple.html └── simple.js ├── index.js ├── package.json ├── src ├── CascadeSelect.jsx └── index.js └── tests ├── CascadeSelect.spec.js ├── index.spec.js └── runner.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*.{js,css}] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea/ 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | .build 21 | node_modules 22 | .cache 23 | dist 24 | assets/**/*.css 25 | build 26 | lib -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], 3 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], 4 | "requireSpacesInFunctionExpression": { 5 | "beforeOpeningCurlyBrace": true 6 | }, 7 | "disallowSpacesInsideArrayBrackets": true, 8 | "disallowSpacesInsideObjectBrackets": true, 9 | "disallowSpacesInsideParentheses": true, 10 | "disallowQuotedKeysInObjects": "allButReserved", 11 | "disallowSpaceAfterObjectKeys": true, 12 | "requireSpaceBeforeBinaryOperators": ["-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=" ], 13 | "requireSpacesInConditionalExpression": { 14 | "afterTest": true, 15 | "beforeConsequent": true, 16 | "afterConsequent": true, 17 | "beforeAlternate": true 18 | }, 19 | "requireSpaceAfterBinaryOperators": ["/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 20 | "disallowKeywords": [ "with" ], 21 | "disallowSpaceAfterPrefixUnaryOperators": [ "!" , "++", "--", "+", "-", "~"], 22 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--", ","], 23 | "disallowMultipleLineBreaks": true, 24 | "disallowKeywordsOnNewLine": ["else"], 25 | "safeContextKeyword": "self", 26 | "excludeFiles": ["lib/**/parser.js"] 27 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "freeze": true, 6 | "indent": 4, 7 | "latedef": "nofunc", 8 | "quotmark": "false", 9 | "nonew": true, 10 | "newcap": false, 11 | "immed": true, 12 | "noarg": true, 13 | "eqnull": true, 14 | "trailing": true, 15 | "undef": true, 16 | "unused": true, 17 | "browser": true, 18 | "node": true, 19 | "esnext": true, 20 | "globals": { 21 | "describe": false, 22 | "expect": false, 23 | "beforeEach": false, 24 | "afterEach": false, 25 | "modulex": false, 26 | "it": false 27 | } 28 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.cfg 3 | nohup.out 4 | *.iml 5 | .idea/ 6 | .ipr 7 | .iws 8 | *~ 9 | ~* 10 | *.diff 11 | *.log 12 | *.patch 13 | *.bak 14 | .DS_Store 15 | Thumbs.db 16 | .project 17 | .*proj 18 | .svn/ 19 | *.swp 20 | out/ 21 | .build 22 | node_modules 23 | .cache 24 | examples 25 | tests 26 | src 27 | /index.js 28 | .* 29 | assets/**/*.less -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: 5 | - yiminghe@gmail.com 6 | 7 | node_js: 8 | - 0.12 9 | 10 | before_install: 11 | - | 12 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/' 13 | then 14 | echo "Only docs were updated, stopping build process." 15 | exit 16 | fi 17 | npm install mocha-phantomjs -g 18 | phantomjs --version 19 | 20 | script: 21 | - | 22 | if [ "$TEST_TYPE" = test ]; then 23 | npm test 24 | else 25 | npm run $TEST_TYPE 26 | fi 27 | 28 | env: 29 | matrix: 30 | - TEST_TYPE=lint 31 | - TEST_TYPE=browser-test 32 | - TEST_TYPE=browser-test-cover 33 | - TEST_TYPE=saucelabs 34 | global: 35 | - secure: WEgyncQBcyb1NS5VQ/4UikFZ1NuBSRSU5lx0KOpjy6xPylj349kWGf33NddOlq20uJlZYyln6ZHghGSS5ovZJNXOZif3OgRmDRtOjVSYkYHzdTEE5i9lfr7KYK3WkEq0Vk+Lx9CtkjUTcN/JnwbWAjGMdE1kiQj1O2tE0tuJNlM= 36 | - secure: jMVNnTmgXNCAiCS0Akg2TkrhMNxMh5a+sikAg0Hsor+c7qPKsh7VFfCGiS7r3jaW3UtFAXvlz36m9XPT+zTLyCTuvR5gTChR6LXYvx9iLiDxhdpvqoW+ePztBrlXrOzM+jZKYEer7cRCs5ZRjMxM6GjFe87VvoaA3VxWqz8dNp0= 37 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-component/cascade-select/967ea2527161e5e1385bc11d6315f61df877a84b/HISTORY.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rc-cascade-select 2 | --- 3 | 4 | cascade-select ui component for react. 5 | 6 | [![NPM version][npm-image]][npm-url] 7 | [![SPM version](http://spmjs.io/badge/rc-cascade-select)](http://spmjs.io/package/rc-cascade-select) 8 | [![build status][travis-image]][travis-url] 9 | [![Test coverage][coveralls-image]][coveralls-url] 10 | [![gemnasium deps][gemnasium-image]][gemnasium-url] 11 | [![node version][node-image]][node-url] 12 | [![npm download][download-image]][download-url] 13 | [![Sauce Test Status](https://saucelabs.com/buildstatus/rc-cascade-select)](https://saucelabs.com/u/rc-cascade-select) 14 | 15 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/rc-cascade-select.svg)](https://saucelabs.com/u/rc-cascade-select) 16 | 17 | [npm-image]: http://img.shields.io/npm/v/rc-cascade-select.svg?style=flat-square 18 | [npm-url]: http://npmjs.org/package/rc-cascade-select 19 | [travis-image]: https://img.shields.io/travis/react-component/cascade-select.svg?style=flat-square 20 | [travis-url]: https://travis-ci.org/react-component/cascade-select 21 | [coveralls-image]: https://img.shields.io/coveralls/react-component/cascade-select.svg?style=flat-square 22 | [coveralls-url]: https://coveralls.io/r/react-component/cascade-select?branch=master 23 | [gemnasium-image]: http://img.shields.io/gemnasium/react-component/cascade-select.svg?style=flat-square 24 | [gemnasium-url]: https://gemnasium.com/react-component/cascade-select 25 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square 26 | [node-url]: http://nodejs.org/download/ 27 | [download-image]: https://img.shields.io/npm/dm/rc-cascade-select.svg?style=flat-square 28 | [download-url]: https://npmjs.org/package/rc-cascade-select 29 | 30 | ## Feature 31 | 32 | * support ie8,ie8+,chrome,firefox,safari 33 | 34 | ## install 35 | 36 | [![rc-cascade-select](https://nodei.co/npm/rc-cascade-select.png)](https://npmjs.org/package/rc-cascade-select) 37 | 38 | 39 | ## Development 40 | 41 | ``` 42 | npm install 43 | npm start 44 | ``` 45 | 46 | ## Example 47 | 48 | http://localhost:8000/examples/ 49 | 50 | online example: http://react-component.github.io/cascade-select/build/examples/ 51 | 52 | 53 | ## Api 54 | 55 | ### CascadeSelect props 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
nametypedefaultdescription
classNameStringadditional css class of root dom node
valueString[][]current value like input's value
allTextStringall
loaderFunction(value, callback:Function(error, data:Object[])called when select a option, return children options corresponding to current option, data's child type must be type of {name,value}
93 | 94 | ## Test Case 95 | 96 | http://localhost:8000/tests/runner.html?coverage 97 | 98 | ## Coverage 99 | 100 | http://localhost:8000/node_modules/rc-server/node_modules/node-jscover/lib/front-end/jscoverage.html?w=http://localhost:8000/tests/runner.html?coverage 101 | 102 | ## License 103 | 104 | rc-cascade-select is released under the MIT license. 105 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // use jsx to render html, do not modify simple.html 4 | var CascadeSelect = require('rc-cascade-select'); 5 | var React = require('react'); 6 | 7 | function simulateData(value) { 8 | var data = []; 9 | for (var i = 0; i < 4; i++) { 10 | data.push({ 11 | name: value + '_' + i + ' : data', 12 | value: value + '_' + i 13 | }) 14 | } 15 | return data; 16 | } 17 | 18 | class Component extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | value: [] 23 | }; 24 | } 25 | 26 | onChange(value) { 27 | console.log('value changed', value); 28 | this.setState({ 29 | value: value 30 | }); 31 | } 32 | 33 | load(id, callback) { 34 | console.log('load data for: ' + id); 35 | callback(null, simulateData(id || '1')); 36 | } 37 | 38 | render() { 39 | return 44 | 45 | 46 | 47 | 48 | 49 | ; 50 | } 51 | } 52 | 53 | React.render(
54 |

simple cascade select

55 |
56 |
, document.getElementById('__react-content')); 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./src/'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-cascade-select", 3 | "version": "1.0.4", 4 | "description": "cascade-select ui component for react", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-cascade-select", 9 | "cascade-select" 10 | ], 11 | "main": "./lib/index", 12 | "homepage": "http://github.com/react-component/cascade-select", 13 | "author": "yiminghe@gmail.com", 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:react-component/cascade-select.git" 17 | }, 18 | "bugs": { 19 | "url": "http://github.com/react-component/cascade-select/issues" 20 | }, 21 | "licenses": "MIT", 22 | "config": { 23 | "port": 8000 24 | }, 25 | "scripts": { 26 | "build": "rc-tools run build", 27 | "precommit": "rc-tools run precommit", 28 | "less": "rc-tools run less", 29 | "gh-pages": "rc-tools run gh-pages", 30 | "history": "rc-tools run history", 31 | "start": "node --harmony node_modules/.bin/rc-server", 32 | "publish": "rc-tools run tag", 33 | "lint": "rc-tools run lint", 34 | "saucelabs": "node --harmony node_modules/.bin/rc-tools run saucelabs", 35 | "browser-test": "node --harmony node_modules/.bin/rc-tools run browser-test", 36 | "browser-test-cover": "node --harmony node_modules/.bin/rc-tools run browser-test-cover" 37 | }, 38 | "devDependencies": { 39 | "expect.js": "~0.3.1", 40 | "node-dev": "2.x", 41 | "precommit-hook": "^1.0.7", 42 | "rc-server": "3.x", 43 | "rc-tools": "3.x", 44 | "react": "~0.13.0" 45 | }, 46 | "precommit": [ 47 | "precommit" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /src/CascadeSelect.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * yiminghe@gmail.com 5 | * cascade select 6 | */ 7 | var React = require('react'); 8 | 9 | function series(datas, fn, end) { 10 | var next = (error)=> { 11 | if (error) { 12 | end(error); 13 | } else { 14 | start++; 15 | if (start === datas.length) { 16 | end(); 17 | return; 18 | } 19 | fn(datas[start], next); 20 | } 21 | }; 22 | var start = -1; 23 | next(); 24 | } 25 | 26 | function findFromChildrenOfParent(parentData, id) { 27 | var children = parentData && parentData.children || []; 28 | var i; 29 | for (i = 0; i < children.length; i++) { 30 | if (String(children[i].value) === id) { 31 | return children[i]; 32 | } 33 | } 34 | } 35 | 36 | function addOptions(self, children) { 37 | var i = 0; 38 | var value = self.props.value; 39 | var parentData = self.state.data; 40 | var doIt = (doChildren) => { 41 | // depth first 42 | return React.Children.map(doChildren, (c) => { 43 | if (c.type === 'select') { 44 | var newProps = {}; 45 | var v = value[i] || ''; 46 | var data = parentData && parentData.children || []; 47 | if (v) { 48 | parentData = findFromChildrenOfParent(parentData, v); 49 | } else { 50 | parentData = null; 51 | } 52 | newProps.onChange = self.select.bind(self, i); 53 | newProps.value = v; 54 | var options = [].concat(data.map((d) => { 55 | return ; 56 | })); 57 | c = React.cloneElement(c, newProps, options); 58 | i++; 59 | } else if (c.props && c.props.children) { 60 | c = React.cloneElement(c, null, doIt(c.props.children)); 61 | } 62 | return c; 63 | }); 64 | }; 65 | 66 | var ret = doIt(children); 67 | self.selectCount = i; 68 | return ret; 69 | } 70 | 71 | function complementTreeByValue(self, data, value) { 72 | var time = self.time; 73 | if (value && value.length) { 74 | var parentData = data; 75 | var i = 0; 76 | series(value, (id, callback) => { 77 | i++; 78 | var node = findFromChildrenOfParent(parentData, id); 79 | parentData = node; 80 | if (!node) { 81 | callback('end'); 82 | return; 83 | } 84 | if (node.children) { 85 | callback(); 86 | return; 87 | } 88 | if (i === self.selectCount) { 89 | callback(); 90 | return; 91 | } 92 | self.props.loader(id, (error, ret) => { 93 | if (error) { 94 | callback(error); 95 | return; 96 | } 97 | node.children = ret; 98 | callback(); 99 | }); 100 | }, ()=> { 101 | if (self.time === time) { 102 | self.setState({ 103 | data: data 104 | }); 105 | } 106 | }); 107 | } else { 108 | self.setState({ 109 | data: data 110 | }); 111 | } 112 | } 113 | 114 | class CascadeSelect extends React.Component { 115 | constructor(props) { 116 | super(props); 117 | this.state = { 118 | data: { 119 | children: [] 120 | } 121 | }; 122 | } 123 | 124 | componentWillReceiveProps(nextProps) { 125 | this.time = Date.now(); 126 | complementTreeByValue(this, {children: this.state.data.children}, nextProps.value); 127 | } 128 | 129 | componentDidMount() { 130 | var props = this.props; 131 | props.loader(null, (error, data)=> { 132 | if (error) { 133 | return; 134 | } 135 | this.time = Date.now(); 136 | complementTreeByValue(this, {children: data}, this.props.value); 137 | }); 138 | } 139 | 140 | select(index, e) { 141 | var value = this.props.value.concat([]); 142 | for (var i = index + 1; i < value.length; i++) { 143 | value[i] = ''; 144 | } 145 | value[index] = e.target.value; 146 | this.props.onChange(value); 147 | } 148 | 149 | render() { 150 | var props = this.props; 151 | var children = addOptions(this, props.children); 152 | return {children}; 153 | } 154 | } 155 | 156 | CascadeSelect.defaultProps = { 157 | value: [], 158 | allText: 'all', 159 | onChange() { 160 | }, 161 | loader() { 162 | } 163 | }; 164 | 165 | module.exports = CascadeSelect; 166 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./CascadeSelect'); 4 | -------------------------------------------------------------------------------- /tests/CascadeSelect.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var expect = require('expect.js'); 5 | var CascadeSelect = require('../'); 6 | var TestUtils = React.addons.TestUtils; 7 | var Simulate = TestUtils.Simulate; 8 | 9 | function simulateData(value) { 10 | var data = []; 11 | for (var i = 0; i < 2; i++) { 12 | data.push({ 13 | name: value + '_' + i + ' : data', 14 | value: value + '_' + i 15 | }) 16 | } 17 | return data; 18 | } 19 | 20 | class Component extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | this.state = { 24 | value: [] 25 | }; 26 | } 27 | 28 | onChange(value) { 29 | //console.log('value changed', value); 30 | this.setState({ 31 | value: value 32 | }); 33 | } 34 | 35 | load(id, callback) { 36 | //console.log('load data for: ' + id); 37 | callback(null, simulateData(id || '1')); 38 | } 39 | 40 | render() { 41 | return 47 | 48 | 49 | 50 | 51 | ; 52 | } 53 | } 54 | 55 | describe('cascade-select', ()=> { 56 | var component; 57 | var div = document.createElement('div'); 58 | document.body.appendChild(div); 59 | 60 | beforeEach(()=> { 61 | component = React.render(, div); 62 | }); 63 | 64 | afterEach(()=> { 65 | React.unmountComponentAtNode(div); 66 | }); 67 | 68 | it('simple works', ()=> { 69 | var options = TestUtils.scryRenderedDOMComponentsWithTag(component.refs.s1, 'option'); 70 | var data = options.map(function (o) { 71 | return { 72 | name: o.props.children, 73 | value: o.props.value 74 | } 75 | }); 76 | expect(data).to.eql([{"name": "i want all", "value": ""}, { 77 | "name": "1_0 : data", 78 | "value": "1_0" 79 | }, {"name": "1_1 : data", "value": "1_1"}]); 80 | options = TestUtils.scryRenderedDOMComponentsWithTag(component.refs.s2, 'option'); 81 | data = options.map(function (o) { 82 | return { 83 | name: o.props.children, 84 | value: o.props.value 85 | } 86 | }); 87 | expect(data).to.eql([{"name": "i want all", "value": ""}]); 88 | }); 89 | 90 | it('cascade works', ()=> { 91 | component.refs.s1.getDOMNode().selectedIndex = 1; 92 | Simulate.change(component.refs.s1.getDOMNode()); 93 | var options = TestUtils.scryRenderedDOMComponentsWithTag(component.refs.s2, 'option'); 94 | var data = options.map(function (o) { 95 | return { 96 | name: o.props.children, 97 | value: o.props.value 98 | } 99 | }); 100 | expect(data).to.eql([{ 101 | "name": "i want all", 102 | "value": "" 103 | }, { 104 | "name": "1_0_0 : data", 105 | "value": "1_0_0" 106 | }, { 107 | "name": "1_0_1 : data", 108 | "value": "1_0_1" 109 | }]); 110 | expect(component.refs.s2.getDOMNode().selectedIndex).to.be(0); 111 | 112 | component.refs.s2.getDOMNode().selectedIndex = 1; 113 | Simulate.change(component.refs.s2.getDOMNode()); 114 | 115 | component.refs.s1.getDOMNode().selectedIndex = 2; 116 | Simulate.change(component.refs.s1.getDOMNode()); 117 | options = TestUtils.scryRenderedDOMComponentsWithTag(component.refs.s2, 'option'); 118 | data = options.map(function (o) { 119 | return { 120 | name: o.props.children, 121 | value: o.props.value 122 | } 123 | }); 124 | expect(data).to.eql([{ 125 | "name": "i want all", 126 | "value": "" 127 | }, { 128 | "name": "1_1_0 : data", 129 | "value": "1_1_0" 130 | }, { 131 | "name": "1_1_1 : data", 132 | "value": "1_1_1" 133 | }]); 134 | expect(component.refs.s2.getDOMNode().selectedIndex).to.be(0); 135 | }); 136 | 137 | }); 138 | -------------------------------------------------------------------------------- /tests/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./CascadeSelect.spec'); 4 | -------------------------------------------------------------------------------- /tests/runner.html: -------------------------------------------------------------------------------- 1 | stub --------------------------------------------------------------------------------