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

Convert select/option to not use wrappers

Ben Alpert committed Jun 4, 2015 at 18:13 UTC abfd151b90c54510d0a7a5b4404c443688510c35
5 files changed +158 -156
src/renderers/dom/client/wrappers/ReactDOMOption.js
+19 -38
@@ -11,86 +11,66 @@
11
12 'use strict';
13
14 -var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
14 var ReactChildren = require('ReactChildren');
16 -var ReactClass = require('ReactClass');
15 var ReactDOMSelect = require('ReactDOMSelect');
18 -var ReactElement = require('ReactElement');
19 -var ReactPropTypes = require('ReactPropTypes');
16
17 var assign = require('Object.assign');
18 var warning = require('warning');
19
24 -var option = ReactElement.createFactory('option');
25 -
20 var valueContextKey = ReactDOMSelect.valueContextKey;
21
22 /**
23 * Implements an <option> native component that warns when `selected` is set.
24 */
31 -var ReactDOMOption = ReactClass.createClass({
32 - displayName: 'ReactDOMOption',
33 - tagName: 'OPTION',
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() {
25 +var ReactDOMOption = {
26 + mountWrapper: function(inst, props, context) {
27 // TODO (yungsters): Remove support for `selected` in <option>.
28 if (__DEV__) {
29 warning(
51 - this.props.selected == null,
30 + props.selected == null,
31 'Use the `defaultValue` or `value` props on <select> instead of ' +
32 'setting `selected` on <option>.'
33 );
34 }
35
57 - // Look up whether this option is 'selected' via parent-based context
58 - var context = this.context;
36 + // Look up whether this option is 'selected' via context
37 var selectValue = context[valueContextKey];
38
39 // If context key is null (e.g., no specified value or after initial mount)
62 - // or missing (e.g., for <datalist>) skip props
40 + // or missing (e.g., for <datalist>), we don't change props.selected
41 + var selected = null;
42 if (selectValue != null) {
64 - var selected = false;
43 + selected = false;
44 if (Array.isArray(selectValue)) {
45 // multiple
46 for (var i = 0; i < selectValue.length; i++) {
68 - if ('' + selectValue[i] === '' + this.props.value) {
47 + if ('' + selectValue[i] === '' + props.value) {
48 selected = true;
49 break;
50 }
51 }
52 } else {
74 - selected = ('' + selectValue === '' + this.props.value);
53 + selected = ('' + selectValue === '' + props.value);
54 }
76 - this.setState({selected: selected});
55 }
56 +
57 + inst._wrapperState = {selected: selected};
58 },
59
80 - render: function() {
81 - var props = this.props;
60 + getNativeProps: function(inst, props, context) {
61 + var nativeProps = assign({selected: undefined, children: undefined}, props);
62
63 // Read state only from initial mount because <select> updates value
64 // manually; we need the initial state only for server rendering
85 - if (this.state.selected != null) {
86 - props = assign({}, props, {selected: this.state.selected});
65 + if (inst._wrapperState.selected != null) {
66 + nativeProps.selected = inst._wrapperState.selected;
67 }
68
69 var content = '';
70
71 // Flatten children and warn if they aren't strings or numbers;
72 // invalid types are ignored.
93 - ReactChildren.forEach(this.props.children, function(child) {
73 + ReactChildren.forEach(props.children, function(child) {
74 if (child == null) {
75 return;
76 }
@@ -104,9 +84,10 @@ var ReactDOMOption = ReactClass.createClass({
84 }
85 });
86
107 - return option(props, content);
87 + nativeProps.children = content;
88 + return nativeProps;
89 },
90
110 -});
91 +};
92
93 module.exports = ReactDOMOption;
src/renderers/dom/client/wrappers/ReactDOMSelect.js
+98 -100
@@ -11,68 +11,89 @@
11
12 'use strict';
13
14 -var AutoFocusUtils = require('AutoFocusUtils');
14 var LinkedValueUtils = require('LinkedValueUtils');
16 -var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
17 -var ReactClass = require('ReactClass');
18 -var ReactElement = require('ReactElement');
19 -var ReactInstanceMap = require('ReactInstanceMap');
15 +var ReactMount = require('ReactMount');
16 var ReactUpdates = require('ReactUpdates');
21 -var ReactPropTypes = require('ReactPropTypes');
17
18 var assign = require('Object.assign');
24 -var findDOMNode = require('findDOMNode');
25 -
26 -var select = ReactElement.createFactory('select');
19 +var warning = require('warning');
20
21 var valueContextKey =
22 '__ReactDOMSelect_value$' + Math.random().toString(36).slice(2);
23
24 function updateOptionsIfPendingUpdateAndMounted() {
32 - if (this._pendingUpdate) {
33 - this._pendingUpdate = false;
34 - var value = LinkedValueUtils.getValue(this.props);
35 - if (value != null && this.isMounted()) {
36 - updateOptions(this, value);
25 + if (this._wrapperState.pendingUpdate && this._rootNodeID) {
26 + this._wrapperState.pendingUpdate = false;
27 +
28 + var props = this._currentElement.props;
29 + var value = LinkedValueUtils.getValue(props);
30 +
31 + if (value != null) {
32 + updateOptions(this, props, value);
33 }
34 }
35 }
36
37 +function getDeclarationErrorAddendum(owner) {
38 + if (owner) {
39 + var name = owner.getName();
40 + if (name) {
41 + return ' Check the render method of `' + name + '`.';
42 + }
43 + }
44 + return '';
45 +}
46 +
47 +var valuePropNames = ['value', 'defaultValue'];
48 +
49 /**
50 * Validation function for `value` and `defaultValue`.
51 * @private
52 */
45 -function selectValueType(props, propName, componentName) {
46 - if (props[propName] == null) {
47 - return null;
48 - }
49 - if (props.multiple) {
50 - if (!Array.isArray(props[propName])) {
51 - return new Error(
52 - `The \`${propName}\` prop supplied to <select> must be an array if ` +
53 - `\`multiple\` is true.`
54 - );
53 +function checkSelectPropTypes(inst, props) {
54 + var owner = inst._currentElement._owner;
55 + LinkedValueUtils.checkPropTypes(
56 + 'select',
57 + props,
58 + owner
59 + );
60 +
61 + for (var i = 0; i < valuePropNames.length; i++) {
62 + var propName = valuePropNames[i];
63 + if (props[propName] == null) {
64 + continue;
65 }
56 - } else {
57 - if (Array.isArray(props[propName])) {
58 - return new Error(
59 - `The \`${propName}\` prop supplied to <select> must be a scalar ` +
60 - `value if \`multiple\` is false.`
66 + if (props.multiple) {
67 + warning(
68 + Array.isArray(props[propName]),
69 + 'The `%s` prop supplied to <select> must be an array if ' +
70 + '`multiple` is true.%s',
71 + propName,
72 + getDeclarationErrorAddendum(owner)
73 + );
74 + } else {
75 + warning(
76 + !Array.isArray(props[propName]),
77 + 'The `%s` prop supplied to <select> must be a scalar ' +
78 + 'value if `multiple` is false.%s',
79 + propName,
80 + getDeclarationErrorAddendum(owner)
81 );
82 }
83 }
84 }
85
86 /**
67 - * @param {ReactComponent} component Instance of ReactDOMSelect
87 + * @param {ReactDOMComponent} inst
88 + * @param {boolean} multiple
89 * @param {*} propValue A stringable (with `multiple`, a list of stringables).
90 * @private
91 */
71 -function updateOptions(component, propValue) {
92 +function updateOptions(inst, multiple, propValue) {
93 var selectedValue, i;
73 - var options = findDOMNode(component).options;
94 + var options = ReactMount.getNode(inst._rootNodeID).options;
95
75 - if (component.props.multiple) {
96 + if (multiple) {
97 selectedValue = {};
98 for (i = 0; i < propValue.length; i++) {
99 selectedValue['' + propValue[i]] = true;
@@ -114,94 +135,71 @@ function updateOptions(component, propValue) {
135 * If `defaultValue` is provided, any options with the supplied values will be
136 * selected.
137 */
117 -var ReactDOMSelect = ReactClass.createClass({
118 - displayName: 'ReactDOMSelect',
119 - tagName: 'SELECT',
120 -
121 - mixins: [AutoFocusUtils.Mixin, ReactBrowserComponentMixin],
122 -
123 - statics: {
124 - valueContextKey: valueContextKey,
138 +var ReactDOMSelect = {
139 + valueContextKey: valueContextKey,
140 +
141 + getNativeProps: function(inst, props, context) {
142 + return assign({}, props, {
143 + onChange: inst._wrapperState.onChange,
144 + value: undefined,
145 + });
146 },
147
127 - propTypes: {
128 - defaultValue: selectValueType,
129 - value: selectValueType,
130 - },
131 -
132 - componentWillMount: function() {
133 - LinkedValueUtils.checkPropTypes(
134 - 'select',
135 - this.props,
136 - ReactInstanceMap.get(this)._currentElement._owner
137 - );
148 + mountWrapper: function(inst, props) {
149 + if (__DEV__) {
150 + checkSelectPropTypes(inst, props);
151 + }
152
139 - this._pendingUpdate = false;
153 + var value = LinkedValueUtils.getValue(props);
154 + inst._wrapperState = {
155 + pendingUpdate: false,
156 + initialValue: value != null ? value : props.defaultValue,
157 + onChange: _handleChange.bind(inst),
158 + wasMultiple: Boolean(props.multiple),
159 + };
160 },
161
142 - getInitialState: function() {
162 + processChildContext: function(inst, props, context) {
163 // Pass down initial value so initial generated markup has correct
164 // `selected` attributes
145 - var value = LinkedValueUtils.getValue(this.props);
146 - if (value != null) {
147 - return {initialValue: value};
148 - } else {
149 - return {initialValue: this.props.defaultValue};
150 - }
151 - },
152 -
153 - childContextTypes: (function() {
154 - var obj = {};
155 - obj[valueContextKey] = ReactPropTypes.any;
156 - return obj;
157 - })(),
158 -
159 - getChildContext: function() {
160 - var obj = {};
161 - obj[valueContextKey] = this.state.initialValue;
162 - return obj;
165 + var childContext = assign({}, context);
166 + childContext[valueContextKey] = inst._wrapperState.initialValue;
167 + return childContext;
168 },
169
165 - render: function() {
166 - // Clone `this.props` so we don't mutate the input.
167 - var props = assign({}, this.props);
170 + postUpdateWrapper: function(inst) {
171 + var props = inst._currentElement.props;
172
169 - props.onChange = this._handleChange;
170 - props.value = null;
171 -
172 - return select(props, this.props.children);
173 - },
174 -
175 - componentWillReceiveProps: function(nextProps) {
173 // After the initial mount, we control selected-ness manually so don't pass
174 // the context value down
178 - this.setState({initialValue: null});
179 - },
175 + inst._wrapperState.initialValue = undefined;
176
181 - componentDidUpdate: function(prevProps) {
182 - var value = LinkedValueUtils.getValue(this.props);
177 + var wasMultiple = inst._wrapperState.wasMultiple;
178 + inst._wrapperState.wasMultiple = Boolean(props.multiple);
179 +
180 + var value = LinkedValueUtils.getValue(props);
181 if (value != null) {
184 - this._pendingUpdate = false;
185 - updateOptions(this, value);
186 - } else if (!prevProps.multiple !== !this.props.multiple) {
182 + inst._wrapperState.pendingUpdate = false;
183 + updateOptions(inst, Boolean(props.multiple), value);
184 + } else if (wasMultiple !== Boolean(props.multiple)) {
185 // For simplicity, reapply `defaultValue` if `multiple` is toggled.
188 - if (this.props.defaultValue != null) {
189 - updateOptions(this, this.props.defaultValue);
186 + if (props.defaultValue != null) {
187 + updateOptions(inst, Boolean(props.multiple), props.defaultValue);
188 } else {
189 // Revert the select back to its default unselected state.
192 - updateOptions(this, this.props.multiple ? [] : '');
190 + updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
191 }
192 }
193 },
194 +};
195
197 - _handleChange: function(event) {
198 - var returnValue = LinkedValueUtils.executeOnChange(this.props, event);
199 -
200 - this._pendingUpdate = true;
201 - ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
202 - return returnValue;
203 - },
196 +function _handleChange(event) {
197 + var props = this._currentElement.props;
198 + var returnValue = LinkedValueUtils.executeOnChange(props, event);
199
205 -});
200 + this._wrapperState.pendingUpdate = true;
201 + ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
202 + return returnValue;
203 +}
204
205 module.exports = ReactDOMSelect;
src/renderers/dom/client/wrappers/__tests__/ReactDOMSelect-test.js
+9 -9
@@ -323,9 +323,9 @@ describe('ReactDOMSelect', function() {
323 <option value="gorilla">A gorilla!</option>
324 </select>;
325 var markup = React.renderToString(stub);
326 - expect(markup).toContain('<option value="giraffe" selected=');
327 - expect(markup).not.toContain('<option value="monkey" selected=');
328 - expect(markup).not.toContain('<option value="gorilla" selected=');
326 + expect(markup).toContain('<option selected="" value="giraffe"');
327 + expect(markup).not.toContain('<option selected="" value="monkey"');
328 + expect(markup).not.toContain('<option selected="" value="gorilla"');
329 });
330
331 it('should support server-side rendering with defaultValue', function() {
@@ -336,9 +336,9 @@ describe('ReactDOMSelect', function() {
336 <option value="gorilla">A gorilla!</option>
337 </select>;
338 var markup = React.renderToString(stub);
339 - expect(markup).toContain('<option value="giraffe" selected=');
340 - expect(markup).not.toContain('<option value="monkey" selected=');
341 - expect(markup).not.toContain('<option value="gorilla" selected=');
339 + expect(markup).toContain('<option selected="" value="giraffe"');
340 + expect(markup).not.toContain('<option selected="" value="monkey"');
341 + expect(markup).not.toContain('<option selected="" value="gorilla"');
342 });
343
344 it('should support server-side rendering with multiple', function() {
@@ -349,9 +349,9 @@ describe('ReactDOMSelect', function() {
349 <option value="gorilla">A gorilla!</option>
350 </select>;
351 var markup = React.renderToString(stub);
352 - expect(markup).toContain('<option value="giraffe" selected=');
353 - expect(markup).toContain('<option value="gorilla" selected=');
354 - expect(markup).not.toContain('<option value="monkey" selected=');
352 + expect(markup).toContain('<option selected="" value="giraffe"');
353 + expect(markup).toContain('<option selected="" value="gorilla"');
354 + expect(markup).not.toContain('<option selected="" value="monkey"');
355 });
356
357 it('should not control defaultValue if readding options', function() {
src/renderers/dom/shared/ReactDOMComponent.js
+32 -2
@@ -24,6 +24,8 @@ var ReactComponentBrowserEnvironment =
24 require('ReactComponentBrowserEnvironment');
25 var ReactDOMButton = require('ReactDOMButton');
26 var ReactDOMInput = require('ReactDOMInput');
27 +var ReactDOMOption = require('ReactDOMOption');
28 +var ReactDOMSelect = require('ReactDOMSelect');
29 var ReactDOMTextarea = require('ReactDOMTextarea');
30 var ReactMount = require('ReactMount');
31 var ReactMultiChild = require('ReactMultiChild');
@@ -229,6 +231,10 @@ function trapBubbledEventsLocal() {
231 }
232 }
233
234 +function postUpdateSelectWrapper() {
235 + ReactDOMSelect.postUpdateWrapper(this);
236 +}
237 +
238 // For HTML, certain tags should omit their close tag. We keep a whitelist for
239 // those special cased tags.
240
@@ -350,11 +356,20 @@ ReactDOMComponent.Mixin = {
356 props = ReactDOMButton.getNativeProps(this, props, context);
357 break;
358 case 'input':
353 - ReactDOMInput.mountWrapper(this, props);
359 + ReactDOMInput.mountWrapper(this, props, context);
360 props = ReactDOMInput.getNativeProps(this, props, context);
361 break;
362 + case 'option':
363 + ReactDOMOption.mountWrapper(this, props, context);
364 + props = ReactDOMOption.getNativeProps(this, props, context);
365 + break;
366 + case 'select':
367 + ReactDOMSelect.mountWrapper(this, props, context);
368 + props = ReactDOMSelect.getNativeProps(this, props, context);
369 + context = ReactDOMSelect.processChildContext(this, props, context);
370 + break;
371 case 'textarea':
357 - ReactDOMTextarea.mountWrapper(this, props);
372 + ReactDOMTextarea.mountWrapper(this, props, context);
373 props = ReactDOMTextarea.getNativeProps(this, props, context);
374 break;
375 }
@@ -376,6 +391,7 @@ ReactDOMComponent.Mixin = {
391 switch (this._tag) {
392 case 'button':
393 case 'input':
394 + case 'select':
395 case 'textarea':
396 if (props.autoFocus) {
397 transaction.getReactMountReady().enqueue(
@@ -540,6 +556,14 @@ ReactDOMComponent.Mixin = {
556 lastProps = ReactDOMInput.getNativeProps(this, lastProps);
557 nextProps = ReactDOMInput.getNativeProps(this, nextProps);
558 break;
559 + case 'option':
560 + lastProps = ReactDOMOption.getNativeProps(this, lastProps);
561 + nextProps = ReactDOMOption.getNativeProps(this, nextProps);
562 + break;
563 + case 'select':
564 + lastProps = ReactDOMSelect.getNativeProps(this, lastProps);
565 + nextProps = ReactDOMSelect.getNativeProps(this, nextProps);
566 + break;
567 case 'textarea':
568 ReactDOMTextarea.updateWrapper(this);
569 lastProps = ReactDOMTextarea.getNativeProps(this, lastProps);
@@ -555,6 +579,12 @@ ReactDOMComponent.Mixin = {
579 transaction,
580 processChildContext(context, this)
581 );
582 +
583 + if (this._tag === 'select') {
584 + // <select> value update needs to occur after <option> children
585 + // reconciliation
586 + transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
587 + }
588 },
589
590 /**
src/renderers/dom/shared/ReactDefaultInjection.js
-7
@@ -25,8 +25,6 @@ var ReactComponentBrowserEnvironment =
25 var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
26 var ReactDOMComponent = require('ReactDOMComponent');
27 var ReactDOMIDOperations = require('ReactDOMIDOperations');
28 -var ReactDOMOption = require('ReactDOMOption');
29 -var ReactDOMSelect = require('ReactDOMSelect');
28 var ReactDOMTextComponent = require('ReactDOMTextComponent');
29 var ReactElement = require('ReactElement');
30 var ReactEventListener = require('ReactEventListener');
@@ -105,11 +103,6 @@ function inject() {
103
104 ReactInjection.Class.injectMixin(ReactBrowserComponentMixin);
105
108 - ReactInjection.NativeComponent.injectComponentClasses({
109 - 'option': ReactDOMOption,
110 - 'select': ReactDOMSelect,
111 - });
112 -
106 ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
107 ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);
108