Fix server-side rendering of <select>
Fixes #1398.
Ben Alpert committed
Apr 5, 2015 at 12:25 UTC
ea089fdfe62b3193740ee4a30acb6827a905a02c
6 files changed
+200
-45
src/browser/ui/dom/components/LinkedValueUtils.js
+27
-27
@@ -26,26 +26,26 @@ var hasReadOnlyValue = {
26
'submit': true
27
};
28
29
-function _assertSingleLink(input) {
29
+function _assertSingleLink(inputProps) {
30
invariant(
31
- input.props.checkedLink == null || input.props.valueLink == null,
31
+ inputProps.checkedLink == null || inputProps.valueLink == null,
32
'Cannot provide a checkedLink and a valueLink. If you want to use ' +
33
'checkedLink, you probably don\'t want to use valueLink and vice versa.'
34
);
35
}
36
-function _assertValueLink(input) {
37
- _assertSingleLink(input);
36
+function _assertValueLink(inputProps) {
37
+ _assertSingleLink(inputProps);
38
invariant(
39
- input.props.value == null && input.props.onChange == null,
39
+ inputProps.value == null && inputProps.onChange == null,
40
'Cannot provide a valueLink and a value or onChange event. If you want ' +
41
'to use value or onChange, you probably don\'t want to use valueLink.'
42
);
43
}
44
45
-function _assertCheckedLink(input) {
46
- _assertSingleLink(input);
45
+function _assertCheckedLink(inputProps) {
46
+ _assertSingleLink(inputProps);
47
invariant(
48
- input.props.checked == null && input.props.onChange == null,
48
+ inputProps.checked == null && inputProps.onChange == null,
49
'Cannot provide a checkedLink and a checked property or onChange event. ' +
50
'If you want to use checked or onChange, you probably don\'t want to ' +
51
'use checkedLink'
@@ -109,43 +109,43 @@ var LinkedValueUtils = {
109
},
110
111
/**
112
- * @param {ReactComponent} input Form component
112
+ * @param {object} inputProps Props for form component
113
* @return {*} current value of the input either from value prop or link.
114
*/
115
- getValue: function(input) {
116
- if (input.props.valueLink) {
117
- _assertValueLink(input);
118
- return input.props.valueLink.value;
115
+ getValue: function(inputProps) {
116
+ if (inputProps.valueLink) {
117
+ _assertValueLink(inputProps);
118
+ return inputProps.valueLink.value;
119
}
120
- return input.props.value;
120
+ return inputProps.value;
121
},
122
123
/**
124
- * @param {ReactComponent} input Form component
124
+ * @param {object} inputProps Props for form component
125
* @return {*} current checked status of the input either from checked prop
126
* or link.
127
*/
128
- getChecked: function(input) {
129
- if (input.props.checkedLink) {
130
- _assertCheckedLink(input);
131
- return input.props.checkedLink.value;
128
+ getChecked: function(inputProps) {
129
+ if (inputProps.checkedLink) {
130
+ _assertCheckedLink(inputProps);
131
+ return inputProps.checkedLink.value;
132
}
133
- return input.props.checked;
133
+ return inputProps.checked;
134
},
135
136
/**
137
- * @param {ReactComponent} input Form component
137
+ * @param {object} inputProps Props for form component
138
* @return {function} change callback either from onChange prop or link.
139
*/
140
- getOnChange: function(input) {
141
- if (input.props.valueLink) {
142
- _assertValueLink(input);
140
+ getOnChange: function(inputProps) {
141
+ if (inputProps.valueLink) {
142
+ _assertValueLink(inputProps);
143
return _handleLinkedValueChange;
144
- } else if (input.props.checkedLink) {
145
- _assertCheckedLink(input);
144
+ } else if (inputProps.checkedLink) {
145
+ _assertCheckedLink(inputProps);
146
return _handleLinkedCheckChange;
147
}
148
- return input.props.onChange;
148
+ return inputProps.onChange;
149
}
150
};
151
src/browser/ui/dom/components/ReactDOMInput.js
+4
-4
@@ -72,10 +72,10 @@ var ReactDOMInput = ReactClass.createClass({
72
props.defaultChecked = null;
73
props.defaultValue = null;
74
75
- var value = LinkedValueUtils.getValue(this);
75
+ var value = LinkedValueUtils.getValue(this.props);
76
props.value = value != null ? value : this.state.initialValue;
77
78
- var checked = LinkedValueUtils.getChecked(this);
78
+ var checked = LinkedValueUtils.getChecked(this.props);
79
props.checked = checked != null ? checked : this.state.initialChecked;
80
81
props.onChange = this._handleChange;
@@ -104,7 +104,7 @@ var ReactDOMInput = ReactClass.createClass({
104
);
105
}
106
107
- var value = LinkedValueUtils.getValue(this);
107
+ var value = LinkedValueUtils.getValue(this.props);
108
if (value != null) {
109
// Cast `value` to a string to ensure the value is set correctly. While
110
// browsers typically do this as necessary, jsdom doesn't.
@@ -114,7 +114,7 @@ var ReactDOMInput = ReactClass.createClass({
114
115
_handleChange: function(event) {
116
var returnValue;
117
- var onChange = LinkedValueUtils.getOnChange(this);
117
+ var onChange = LinkedValueUtils.getOnChange(this.props);
118
if (onChange) {
119
returnValue = onChange.call(this, event);
120
}
src/browser/ui/dom/components/ReactDOMOption.js
+47
-1
@@ -13,12 +13,18 @@
13
14
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
15
var ReactClass = require('ReactClass');
16
+var ReactDOMSelect = require('ReactDOMSelect');
17
var ReactElement = require('ReactElement');
18
+var ReactInstanceMap = require('ReactInstanceMap');
19
+var ReactPropTypes = require('ReactPropTypes');
20
21
+var assign = require('Object.assign');
22
var warning = require('warning');
23
24
var option = ReactElement.createFactory('option');
25
26
+var valueContextKey = ReactDOMSelect.valueContextKey;
27
+
28
/**
29
* Implements an <option> native component that warns when `selected` is set.
30
*/
@@ -28,6 +34,16 @@ var ReactDOMOption = ReactClass.createClass({
34
35
mixins: [ReactBrowserComponentMixin],
36
37
+ getInitialState: function() {
38
+ return {selected: null};
39
+ },
40
+
41
+ contextTypes: (function() {
42
+ var obj = {};
43
+ obj[valueContextKey] = ReactPropTypes.any;
44
+ return obj;
45
+ })(),
46
+
47
componentWillMount: function() {
48
// TODO (yungsters): Remove support for `selected` in <option>.
49
if (__DEV__) {
@@ -37,10 +53,40 @@ var ReactDOMOption = ReactClass.createClass({
53
'setting `selected` on <option>.'
54
);
55
}
56
+
57
+ // Look up whether this option is 'selected' via parent-based context
58
+ var context = ReactInstanceMap.get(this)._context;
59
+ var selectValue = context[valueContextKey];
60
+
61
+ // If context key is null (e.g., no specified value or after initial mount)
62
+ // or missing (e.g., for <datalist>) skip props
63
+ if (selectValue != null) {
64
+ var selected = false;
65
+ if (Array.isArray(selectValue)) {
66
+ // multiple
67
+ for (var i = 0; i < selectValue.length; i++) {
68
+ if ('' + selectValue[i] === '' + this.props.value) {
69
+ selected = true;
70
+ break;
71
+ }
72
+ }
73
+ } else {
74
+ selected = ('' + selectValue === '' + this.props.value);
75
+ }
76
+ this.setState({selected: selected});
77
+ }
78
},
79
80
render: function() {
43
- return option(this.props, this.props.children);
81
+ var props = this.props;
82
+
83
+ // Read state only from initial mount because <select> updates value
84
+ // manually; we need the initial state only for server rendering
85
+ if (this.state.selected != null) {
86
+ props = assign({}, props, {selected: this.state.selected});
87
+ }
88
+
89
+ return option(props, this.props.children);
90
}
91
92
});
src/browser/ui/dom/components/ReactDOMSelect.js
+38
-10
@@ -17,17 +17,21 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
17
var ReactClass = require('ReactClass');
18
var ReactElement = require('ReactElement');
19
var ReactUpdates = require('ReactUpdates');
20
+var ReactPropTypes = require('ReactPropTypes');
21
22
var assign = require('Object.assign');
23
var findDOMNode = require('findDOMNode');
24
25
var select = ReactElement.createFactory('select');
26
27
+var valueContextKey =
28
+ '__ReactDOMSelect_value$' + Math.random().toString(36).slice(2);
29
+
30
function updateOptionsIfPendingUpdateAndMounted() {
31
/*jshint validthis:true */
32
if (this._pendingUpdate) {
33
this._pendingUpdate = false;
30
- var value = LinkedValueUtils.getValue(this);
34
+ var value = LinkedValueUtils.getValue(this.props);
35
if (value != null && this.isMounted()) {
36
updateOptions(this, value);
37
}
@@ -116,11 +120,38 @@ var ReactDOMSelect = ReactClass.createClass({
120
121
mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin],
122
123
+ statics: {
124
+ valueContextKey: valueContextKey
125
+ },
126
+
127
propTypes: {
128
defaultValue: selectValueType,
129
value: selectValueType
130
},
131
132
+ getInitialState: function() {
133
+ // Pass down initial value so initial generated markup has correct
134
+ // `selected` attributes
135
+ var value = LinkedValueUtils.getValue(this.props);
136
+ if (value != null) {
137
+ return {initialValue: value};
138
+ } else {
139
+ return {initialValue: this.props.defaultValue};
140
+ }
141
+ },
142
+
143
+ childContextTypes: (function() {
144
+ var obj = {};
145
+ obj[valueContextKey] = ReactPropTypes.any;
146
+ return obj;
147
+ })(),
148
+
149
+ getChildContext: function() {
150
+ var obj = {};
151
+ obj[valueContextKey] = this.state.initialValue;
152
+ return obj;
153
+ },
154
+
155
render: function() {
156
// Clone `this.props` so we don't mutate the input.
157
var props = assign({}, this.props);
@@ -135,17 +166,14 @@ var ReactDOMSelect = ReactClass.createClass({
166
this._pendingUpdate = false;
167
},
168
138
- componentDidMount: function() {
139
- var value = LinkedValueUtils.getValue(this);
140
- if (value != null) {
141
- updateOptions(this, value);
142
- } else if (this.props.defaultValue != null) {
143
- updateOptions(this, this.props.defaultValue);
144
- }
169
+ componentWillReceiveProps: function(nextProps) {
170
+ // After the initial mount, we control selected-ness manually so don't pass
171
+ // the context value down
172
+ this.setState({initialValue: null});
173
},
174
175
componentDidUpdate: function(prevProps) {
148
- var value = LinkedValueUtils.getValue(this);
176
+ var value = LinkedValueUtils.getValue(this.props);
177
if (value != null) {
178
this._pendingUpdate = false;
179
updateOptions(this, value);
@@ -162,7 +190,7 @@ var ReactDOMSelect = ReactClass.createClass({
190
191
_handleChange: function(event) {
192
var returnValue;
165
- var onChange = LinkedValueUtils.getOnChange(this);
193
+ var onChange = LinkedValueUtils.getOnChange(this.props);
194
if (onChange) {
195
returnValue = onChange.call(this, event);
196
}
src/browser/ui/dom/components/ReactDOMTextarea.js
+3
-3
@@ -84,7 +84,7 @@ var ReactDOMTextarea = ReactClass.createClass({
84
if (defaultValue == null) {
85
defaultValue = '';
86
}
87
- var value = LinkedValueUtils.getValue(this);
87
+ var value = LinkedValueUtils.getValue(this.props);
88
return {
89
// We save the initial value so that `ReactDOMComponent` doesn't update
90
// `textContent` (unnecessary since we update value).
@@ -113,7 +113,7 @@ var ReactDOMTextarea = ReactClass.createClass({
113
},
114
115
componentDidUpdate: function(prevProps, prevState, prevContext) {
116
- var value = LinkedValueUtils.getValue(this);
116
+ var value = LinkedValueUtils.getValue(this.props);
117
if (value != null) {
118
var rootNode = findDOMNode(this);
119
// Cast `value` to a string to ensure the value is set correctly. While
@@ -124,7 +124,7 @@ var ReactDOMTextarea = ReactClass.createClass({
124
125
_handleChange: function(event) {
126
var returnValue;
127
- var onChange = LinkedValueUtils.getOnChange(this);
127
+ var onChange = LinkedValueUtils.getOnChange(this.props);
128
if (onChange) {
129
returnValue = onChange.call(this, event);
130
}
src/browser/ui/dom/components/__tests__/ReactDOMSelect-test.js
+81
@@ -315,4 +315,85 @@ describe('ReactDOMSelect', function() {
315
expect(link.requestChange.mock.calls[0][0]).toEqual('gorilla');
316
317
});
318
+
319
+ it('should support server-side rendering', function() {
320
+ var stub =
321
+ <select value="giraffe">
322
+ <option value="monkey">A monkey!</option>
323
+ <option value="giraffe">A giraffe!</option>
324
+ <option value="gorilla">A gorilla!</option>
325
+ </select>;
326
+ var markup = React.renderToString(stub);
327
+ expect(markup).toContain('<option value="giraffe" selected=');
328
+ expect(markup).not.toContain('<option value="monkey" selected=');
329
+ expect(markup).not.toContain('<option value="gorilla" selected=');
330
+ });
331
+
332
+ it('should support server-side rendering with defaultValue', function() {
333
+ var stub =
334
+ <select defaultValue="giraffe">
335
+ <option value="monkey">A monkey!</option>
336
+ <option value="giraffe">A giraffe!</option>
337
+ <option value="gorilla">A gorilla!</option>
338
+ </select>;
339
+ var markup = React.renderToString(stub);
340
+ expect(markup).toContain('<option value="giraffe" selected=');
341
+ expect(markup).not.toContain('<option value="monkey" selected=');
342
+ expect(markup).not.toContain('<option value="gorilla" selected=');
343
+ });
344
+
345
+ it('should support server-side rendering with multiple', function() {
346
+ var stub =
347
+ <select multiple={true} value={['giraffe', 'gorilla']}>
348
+ <option value="monkey">A monkey!</option>
349
+ <option value="giraffe">A giraffe!</option>
350
+ <option value="gorilla">A gorilla!</option>
351
+ </select>;
352
+ var markup = React.renderToString(stub);
353
+ expect(markup).toContain('<option value="giraffe" selected=');
354
+ expect(markup).toContain('<option value="gorilla" selected=');
355
+ expect(markup).not.toContain('<option value="monkey" selected=');
356
+ });
357
+
358
+ it('should not control defaultValue if readding options', function() {
359
+ var container = document.createElement('div');
360
+
361
+ var select = React.render(
362
+ <select multiple={true} defaultValue={['giraffe']}>
363
+ <option key="monkey" value="monkey">A monkey!</option>
364
+ <option key="giraffe" value="giraffe">A giraffe!</option>
365
+ <option key="gorilla" value="gorilla">A gorilla!</option>
366
+ </select>,
367
+ container
368
+ );
369
+ var node = React.findDOMNode(select);
370
+
371
+ expect(node.options[0].selected).toBe(false); // monkey
372
+ expect(node.options[1].selected).toBe(true); // giraffe
373
+ expect(node.options[2].selected).toBe(false); // gorilla
374
+
375
+ React.render(
376
+ <select multiple={true} defaultValue={['giraffe']}>
377
+ <option key="monkey" value="monkey">A monkey!</option>
378
+ <option key="gorilla" value="gorilla">A gorilla!</option>
379
+ </select>,
380
+ container
381
+ );
382
+
383
+ expect(node.options[0].selected).toBe(false); // monkey
384
+ expect(node.options[1].selected).toBe(false); // gorilla
385
+
386
+ React.render(
387
+ <select multiple={true} defaultValue={['giraffe']}>
388
+ <option key="monkey" value="monkey">A monkey!</option>
389
+ <option key="giraffe" value="giraffe">A giraffe!</option>
390
+ <option key="gorilla" value="gorilla">A gorilla!</option>
391
+ </select>,
392
+ container
393
+ );
394
+
395
+ expect(node.options[0].selected).toBe(false); // monkey
396
+ expect(node.options[1].selected).toBe(false); // giraffe
397
+ expect(node.options[2].selected).toBe(false); // gorilla
398
+ });
399
});