@samitouri / QOS-React-2 / commits / ea089fdfe6

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 });