Skip to content

Commit d12ae9d

Browse files
committed
Merge pull request #2241 from syranide/selectvalue
ReactDOMSelect makeover, fix edge-case inconsistencies and remove state
2 parents 9edc626 + 2601b6a commit d12ae9d

File tree

2 files changed

+122
-64
lines changed

2 files changed

+122
-64
lines changed

src/browser/ui/dom/components/ReactDOMSelect.js

Lines changed: 51 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ var assign = require('Object.assign');
2222

2323
var select = ReactElement.createFactory('select');
2424

25-
function updateWithPendingValueIfMounted() {
25+
function updateOptionsIfPendingUpdateAndMounted() {
2626
/*jshint validthis:true */
27-
if (this.isMounted()) {
28-
this.setState({value: this._pendingValue});
29-
this._pendingValue = 0;
27+
if (this._pendingUpdate) {
28+
this._pendingUpdate = false;
29+
var value = LinkedValueUtils.getValue(this);
30+
if (value != null && this.isMounted()) {
31+
updateOptions(this, value);
32+
}
3033
}
3134
}
3235

@@ -56,40 +59,43 @@ function selectValueType(props, propName, componentName) {
5659
}
5760

5861
/**
59-
* If `value` is supplied, updates <option> elements on mount and update.
6062
* @param {ReactComponent} component Instance of ReactDOMSelect
61-
* @param {?*} propValue For uncontrolled components, null/undefined. For
62-
* controlled components, a string (or with `multiple`, a list of strings).
63+
* @param {*} propValue A stringable (with `multiple`, a list of stringables).
6364
* @private
6465
*/
6566
function updateOptions(component, propValue) {
66-
var multiple = component.props.multiple;
67-
var value = propValue != null ? propValue : component.state.value;
68-
var options = component.getDOMNode().options;
6967
var selectedValue, i, l;
70-
if (multiple) {
68+
var options = component.getDOMNode().options;
69+
70+
if (component.props.multiple) {
7171
selectedValue = {};
72-
for (i = 0, l = value.length; i < l; ++i) {
73-
selectedValue['' + value[i]] = true;
72+
for (i = 0, l = propValue.length; i < l; i++) {
73+
selectedValue['' + propValue[i]] = true;
74+
}
75+
for (i = 0, l = options.length; i < l; i++) {
76+
var selected = selectedValue.hasOwnProperty(options[i].value);
77+
if (options[i].selected !== selected) {
78+
options[i].selected = selected;
79+
}
7480
}
7581
} else {
76-
selectedValue = '' + value;
77-
}
78-
for (i = 0, l = options.length; i < l; i++) {
79-
var selected = multiple ?
80-
selectedValue.hasOwnProperty(options[i].value) :
81-
options[i].value === selectedValue;
82-
83-
if (selected !== options[i].selected) {
84-
options[i].selected = selected;
82+
// Do not set `select.value` as exact behavior isn't consistent across all
83+
// browsers for all cases.
84+
selectedValue = '' + propValue;
85+
for (i = 0, l = options.length; i < l; i++) {
86+
if (options[i].value === selectedValue) {
87+
options[i].selected = true;
88+
return;
89+
}
8590
}
91+
options[0].selected = true;
8692
}
8793
}
8894

8995
/**
9096
* Implements a <select> native component that allows optionally setting the
9197
* props `value` and `defaultValue`. If `multiple` is false, the prop must be a
92-
* string. If `multiple` is true, the prop must be an array of strings.
98+
* stringable. If `multiple` is true, the prop must be an array of stringables.
9399
*
94100
* If `value` is not supplied (or null/undefined), user actions that change the
95101
* selected option will trigger updates to the rendered options.
@@ -111,22 +117,6 @@ var ReactDOMSelect = ReactClass.createClass({
111117
value: selectValueType
112118
},
113119

114-
getInitialState: function() {
115-
return {value: this.props.defaultValue || (this.props.multiple ? [] : '')};
116-
},
117-
118-
componentWillMount: function() {
119-
this._pendingValue = null;
120-
},
121-
122-
componentWillReceiveProps: function(nextProps) {
123-
if (!this.props.multiple && nextProps.multiple) {
124-
this.setState({value: [this.state.value]});
125-
} else if (this.props.multiple && !nextProps.multiple) {
126-
this.setState({value: this.state.value[0]});
127-
}
128-
},
129-
130120
render: function() {
131121
// Clone `this.props` so we don't mutate the input.
132122
var props = assign({}, this.props);
@@ -137,16 +127,32 @@ var ReactDOMSelect = ReactClass.createClass({
137127
return select(props, this.props.children);
138128
},
139129

130+
componentWillMount: function() {
131+
this._pendingUpdate = false;
132+
},
133+
140134
componentDidMount: function() {
141-
updateOptions(this, LinkedValueUtils.getValue(this));
135+
var value = LinkedValueUtils.getValue(this);
136+
if (value != null) {
137+
updateOptions(this, value);
138+
} else if (this.props.defaultValue != null) {
139+
updateOptions(this, this.props.defaultValue);
140+
}
142141
},
143142

144143
componentDidUpdate: function(prevProps) {
145144
var value = LinkedValueUtils.getValue(this);
146-
var prevMultiple = !!prevProps.multiple;
147-
var multiple = !!this.props.multiple;
148-
if (value != null || prevMultiple !== multiple) {
145+
if (value != null) {
146+
this._pendingUpdate = false;
149147
updateOptions(this, value);
148+
} else if (!prevProps.multiple !== !this.props.multiple) {
149+
// For simplicity, reapply `defaultValue` if `multiple` is toggled.
150+
if (this.props.defaultValue != null) {
151+
updateOptions(this, this.props.defaultValue);
152+
} else {
153+
// Revert the select back to its default unselected state.
154+
updateOptions(this, this.props.multiple ? [] : '');
155+
}
150156
}
151157
},
152158

@@ -157,21 +163,8 @@ var ReactDOMSelect = ReactClass.createClass({
157163
returnValue = onChange.call(this, event);
158164
}
159165

160-
var selectedValue;
161-
if (this.props.multiple) {
162-
selectedValue = [];
163-
var options = event.target.options;
164-
for (var i = 0, l = options.length; i < l; i++) {
165-
if (options[i].selected) {
166-
selectedValue.push(options[i].value);
167-
}
168-
}
169-
} else {
170-
selectedValue = event.target.value;
171-
}
172-
173-
this._pendingValue = selectedValue;
174-
ReactUpdates.asap(updateWithPendingValueIfMounted, this);
166+
this._pendingUpdate = true;
167+
ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
175168
return returnValue;
176169
}
177170

src/browser/ui/dom/components/__tests__/ReactDOMSelect-test.js

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,27 @@ describe('ReactDOMSelect', function() {
137137
expect(node.options[2].selected).toBe(true); // twelve
138138
});
139139

140+
it('should reset child options selected when they are changed and `value` is set', function() {
141+
var stub =
142+
<select multiple={true} value={["a", "b"]}>
143+
</select>
144+
stub = ReactTestUtils.renderIntoDocument(stub);
145+
146+
stub.setProps({
147+
children: [
148+
<option value="a">a</option>,
149+
<option value="b">b</option>,
150+
<option value="c">c</option>
151+
]
152+
})
153+
154+
var node = stub.getDOMNode()
155+
156+
expect(node.options[0].selected).toBe(true); // a
157+
expect(node.options[1].selected).toBe(true); // b
158+
expect(node.options[2].selected).toBe(false); // c
159+
});
160+
140161
it('should allow setting `value` with `objectToString`', function() {
141162
var objectToString = {
142163
animal: "giraffe",
@@ -181,12 +202,12 @@ describe('ReactDOMSelect', function() {
181202
expect(node.options[1].selected).toBe(true); // giraffe
182203
expect(node.options[2].selected).toBe(false); // gorilla
183204

184-
// When making it multiple, giraffe should still be selected
185-
stub.setProps({multiple: true, defaultValue: null});
205+
// When making it multiple, giraffe and gorilla should be selected
206+
stub.setProps({multiple: true, defaultValue: ['giraffe', 'gorilla']});
186207

187208
expect(node.options[0].selected).toBe(false); // monkey
188209
expect(node.options[1].selected).toBe(true); // giraffe
189-
expect(node.options[2].selected).toBe(false); // gorilla
210+
expect(node.options[2].selected).toBe(true); // gorilla
190211
});
191212

192213
it('should allow switching from multiple', function() {
@@ -203,13 +224,57 @@ describe('ReactDOMSelect', function() {
203224
expect(node.options[1].selected).toBe(true); // giraffe
204225
expect(node.options[2].selected).toBe(true); // gorilla
205226

206-
// When removing multiple, giraffe should still be selected (but gorilla
207-
// will no longer be)
208-
stub.setProps({multiple: false, defaultValue: null});
227+
// When removing multiple, defaultValue is applied again, being omitted
228+
// means that "monkey" will be selected
229+
stub.setProps({multiple: false, defaultValue: 'gorilla'});
230+
231+
expect(node.options[0].selected).toBe(false); // monkey
232+
expect(node.options[1].selected).toBe(false); // giraffe
233+
expect(node.options[2].selected).toBe(true); // gorilla
234+
});
235+
236+
it('should remember value when switching to uncontrolled', function() {
237+
var stub =
238+
<select value={'giraffe'}>
239+
<option value="monkey">A monkey!</option>
240+
<option value="giraffe">A giraffe!</option>
241+
<option value="gorilla">A gorilla!</option>
242+
</select>;
243+
stub = ReactTestUtils.renderIntoDocument(stub);
244+
var node = stub.getDOMNode();
209245

210246
expect(node.options[0].selected).toBe(false); // monkey
211247
expect(node.options[1].selected).toBe(true); // giraffe
212248
expect(node.options[2].selected).toBe(false); // gorilla
249+
250+
stub.setProps({value: null});
251+
252+
expect(node.options[0].selected).toBe(false); // monkey
253+
expect(node.options[1].selected).toBe(true); // giraffe
254+
expect(node.options[2].selected).toBe(false); // gorilla
255+
});
256+
257+
it('should remember updated value when switching to uncontrolled', function() {
258+
var stub =
259+
<select value={'giraffe'}>
260+
<option value="monkey">A monkey!</option>
261+
<option value="giraffe">A giraffe!</option>
262+
<option value="gorilla">A gorilla!</option>
263+
</select>;
264+
stub = ReactTestUtils.renderIntoDocument(stub);
265+
var node = stub.getDOMNode();
266+
267+
stub.setProps({value: 'gorilla'});
268+
269+
expect(node.options[0].selected).toBe(false); // monkey
270+
expect(node.options[1].selected).toBe(false); // giraffe
271+
expect(node.options[2].selected).toBe(true); // gorilla
272+
273+
stub.setProps({value: null});
274+
275+
expect(node.options[0].selected).toBe(false); // monkey
276+
expect(node.options[1].selected).toBe(false); // giraffe
277+
expect(node.options[2].selected).toBe(true); // gorilla
213278
});
214279

215280
it('should support ReactLink', function() {

0 commit comments

Comments
 (0)