├── .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 | [](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 | [](https://saucelabs.com/u/rc-cascade-select)
14 |
15 | [](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 | [](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 | name |
61 | type |
62 | default |
63 | description |
64 |
65 |
66 |
67 |
68 | className |
69 | String |
70 | |
71 | additional css class of root dom node |
72 |
73 |
74 | value |
75 | String[] |
76 | [] |
77 | current value like input's value |
78 |
79 |
80 | allText |
81 | String |
82 | all |
83 | |
84 |
85 |
86 | loader |
87 | Function(value, callback:Function(error, data:Object[]) |
88 | |
89 | called when select a option, return children options corresponding to current option, data's child type must be type of {name,value} |
90 |
91 |
92 |
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
--------------------------------------------------------------------------------