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