Merge pull request #4035 from spicyj/dc-os

Convert select/option to not use wrappers
This commit is contained in:
Ben Alpert
2015-06-05 12:41:34 -07:00
5 changed files with 158 additions and 156 deletions
@@ -11,86 +11,66 @@
'use strict';
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
var ReactChildren = require('ReactChildren');
var ReactClass = require('ReactClass');
var ReactDOMSelect = require('ReactDOMSelect');
var ReactElement = require('ReactElement');
var ReactPropTypes = require('ReactPropTypes');
var assign = require('Object.assign');
var warning = require('warning');
var option = ReactElement.createFactory('option');
var valueContextKey = ReactDOMSelect.valueContextKey;
/**
* Implements an <option> native component that warns when `selected` is set.
*/
var ReactDOMOption = ReactClass.createClass({
displayName: 'ReactDOMOption',
tagName: 'OPTION',
mixins: [ReactBrowserComponentMixin],
getInitialState: function() {
return {selected: null};
},
contextTypes: (function() {
var obj = {};
obj[valueContextKey] = ReactPropTypes.any;
return obj;
})(),
componentWillMount: function() {
var ReactDOMOption = {
mountWrapper: function(inst, props, context) {
// TODO (yungsters): Remove support for `selected` in <option>.
if (__DEV__) {
warning(
this.props.selected == null,
props.selected == null,
'Use the `defaultValue` or `value` props on <select> instead of ' +
'setting `selected` on <option>.'
);
}
// Look up whether this option is 'selected' via parent-based context
var context = this.context;
// Look up whether this option is 'selected' via context
var selectValue = context[valueContextKey];
// If context key is null (e.g., no specified value or after initial mount)
// or missing (e.g., for <datalist>) skip props
// or missing (e.g., for <datalist>), we don't change props.selected
var selected = null;
if (selectValue != null) {
var selected = false;
selected = false;
if (Array.isArray(selectValue)) {
// multiple
for (var i = 0; i < selectValue.length; i++) {
if ('' + selectValue[i] === '' + this.props.value) {
if ('' + selectValue[i] === '' + props.value) {
selected = true;
break;
}
}
} else {
selected = ('' + selectValue === '' + this.props.value);
selected = ('' + selectValue === '' + props.value);
}
this.setState({selected: selected});
}
inst._wrapperState = {selected: selected};
},
render: function() {
var props = this.props;
getNativeProps: function(inst, props, context) {
var nativeProps = assign({selected: undefined, children: undefined}, props);
// Read state only from initial mount because <select> updates value
// manually; we need the initial state only for server rendering
if (this.state.selected != null) {
props = assign({}, props, {selected: this.state.selected});
if (inst._wrapperState.selected != null) {
nativeProps.selected = inst._wrapperState.selected;
}
var content = '';
// Flatten children and warn if they aren't strings or numbers;
// invalid types are ignored.
ReactChildren.forEach(this.props.children, function(child) {
ReactChildren.forEach(props.children, function(child) {
if (child == null) {
return;
}
@@ -104,9 +84,10 @@ var ReactDOMOption = ReactClass.createClass({
}
});
return option(props, content);
nativeProps.children = content;
return nativeProps;
},
});
};
module.exports = ReactDOMOption;
@@ -11,68 +11,89 @@
'use strict';
var AutoFocusUtils = require('AutoFocusUtils');
var LinkedValueUtils = require('LinkedValueUtils');
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
var ReactClass = require('ReactClass');
var ReactElement = require('ReactElement');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactMount = require('ReactMount');
var ReactUpdates = require('ReactUpdates');
var ReactPropTypes = require('ReactPropTypes');
var assign = require('Object.assign');
var findDOMNode = require('findDOMNode');
var select = ReactElement.createFactory('select');
var warning = require('warning');
var valueContextKey =
'__ReactDOMSelect_value$' + Math.random().toString(36).slice(2);
function updateOptionsIfPendingUpdateAndMounted() {
if (this._pendingUpdate) {
this._pendingUpdate = false;
var value = LinkedValueUtils.getValue(this.props);
if (value != null && this.isMounted()) {
updateOptions(this, value);
if (this._wrapperState.pendingUpdate && this._rootNodeID) {
this._wrapperState.pendingUpdate = false;
var props = this._currentElement.props;
var value = LinkedValueUtils.getValue(props);
if (value != null) {
updateOptions(this, props, value);
}
}
}
function getDeclarationErrorAddendum(owner) {
if (owner) {
var name = owner.getName();
if (name) {
return ' Check the render method of `' + name + '`.';
}
}
return '';
}
var valuePropNames = ['value', 'defaultValue'];
/**
* Validation function for `value` and `defaultValue`.
* @private
*/
function selectValueType(props, propName, componentName) {
if (props[propName] == null) {
return null;
}
if (props.multiple) {
if (!Array.isArray(props[propName])) {
return new Error(
`The \`${propName}\` prop supplied to <select> must be an array if ` +
`\`multiple\` is true.`
);
function checkSelectPropTypes(inst, props) {
var owner = inst._currentElement._owner;
LinkedValueUtils.checkPropTypes(
'select',
props,
owner
);
for (var i = 0; i < valuePropNames.length; i++) {
var propName = valuePropNames[i];
if (props[propName] == null) {
continue;
}
} else {
if (Array.isArray(props[propName])) {
return new Error(
`The \`${propName}\` prop supplied to <select> must be a scalar ` +
`value if \`multiple\` is false.`
if (props.multiple) {
warning(
Array.isArray(props[propName]),
'The `%s` prop supplied to <select> must be an array if ' +
'`multiple` is true.%s',
propName,
getDeclarationErrorAddendum(owner)
);
} else {
warning(
!Array.isArray(props[propName]),
'The `%s` prop supplied to <select> must be a scalar ' +
'value if `multiple` is false.%s',
propName,
getDeclarationErrorAddendum(owner)
);
}
}
}
/**
* @param {ReactComponent} component Instance of ReactDOMSelect
* @param {ReactDOMComponent} inst
* @param {boolean} multiple
* @param {*} propValue A stringable (with `multiple`, a list of stringables).
* @private
*/
function updateOptions(component, propValue) {
function updateOptions(inst, multiple, propValue) {
var selectedValue, i;
var options = findDOMNode(component).options;
var options = ReactMount.getNode(inst._rootNodeID).options;
if (component.props.multiple) {
if (multiple) {
selectedValue = {};
for (i = 0; i < propValue.length; i++) {
selectedValue['' + propValue[i]] = true;
@@ -114,94 +135,71 @@ function updateOptions(component, propValue) {
* If `defaultValue` is provided, any options with the supplied values will be
* selected.
*/
var ReactDOMSelect = ReactClass.createClass({
displayName: 'ReactDOMSelect',
tagName: 'SELECT',
var ReactDOMSelect = {
valueContextKey: valueContextKey,
mixins: [AutoFocusUtils.Mixin, ReactBrowserComponentMixin],
statics: {
valueContextKey: valueContextKey,
getNativeProps: function(inst, props, context) {
return assign({}, props, {
onChange: inst._wrapperState.onChange,
value: undefined,
});
},
propTypes: {
defaultValue: selectValueType,
value: selectValueType,
mountWrapper: function(inst, props) {
if (__DEV__) {
checkSelectPropTypes(inst, props);
}
var value = LinkedValueUtils.getValue(props);
inst._wrapperState = {
pendingUpdate: false,
initialValue: value != null ? value : props.defaultValue,
onChange: _handleChange.bind(inst),
wasMultiple: Boolean(props.multiple),
};
},
componentWillMount: function() {
LinkedValueUtils.checkPropTypes(
'select',
this.props,
ReactInstanceMap.get(this)._currentElement._owner
);
this._pendingUpdate = false;
},
getInitialState: function() {
processChildContext: function(inst, props, context) {
// Pass down initial value so initial generated markup has correct
// `selected` attributes
var value = LinkedValueUtils.getValue(this.props);
if (value != null) {
return {initialValue: value};
} else {
return {initialValue: this.props.defaultValue};
}
var childContext = assign({}, context);
childContext[valueContextKey] = inst._wrapperState.initialValue;
return childContext;
},
childContextTypes: (function() {
var obj = {};
obj[valueContextKey] = ReactPropTypes.any;
return obj;
})(),
postUpdateWrapper: function(inst) {
var props = inst._currentElement.props;
getChildContext: function() {
var obj = {};
obj[valueContextKey] = this.state.initialValue;
return obj;
},
render: function() {
// Clone `this.props` so we don't mutate the input.
var props = assign({}, this.props);
props.onChange = this._handleChange;
props.value = null;
return select(props, this.props.children);
},
componentWillReceiveProps: function(nextProps) {
// After the initial mount, we control selected-ness manually so don't pass
// the context value down
this.setState({initialValue: null});
},
inst._wrapperState.initialValue = undefined;
componentDidUpdate: function(prevProps) {
var value = LinkedValueUtils.getValue(this.props);
var wasMultiple = inst._wrapperState.wasMultiple;
inst._wrapperState.wasMultiple = Boolean(props.multiple);
var value = LinkedValueUtils.getValue(props);
if (value != null) {
this._pendingUpdate = false;
updateOptions(this, value);
} else if (!prevProps.multiple !== !this.props.multiple) {
inst._wrapperState.pendingUpdate = false;
updateOptions(inst, Boolean(props.multiple), value);
} else if (wasMultiple !== Boolean(props.multiple)) {
// For simplicity, reapply `defaultValue` if `multiple` is toggled.
if (this.props.defaultValue != null) {
updateOptions(this, this.props.defaultValue);
if (props.defaultValue != null) {
updateOptions(inst, Boolean(props.multiple), props.defaultValue);
} else {
// Revert the select back to its default unselected state.
updateOptions(this, this.props.multiple ? [] : '');
updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
}
}
},
};
_handleChange: function(event) {
var returnValue = LinkedValueUtils.executeOnChange(this.props, event);
function _handleChange(event) {
var props = this._currentElement.props;
var returnValue = LinkedValueUtils.executeOnChange(props, event);
this._pendingUpdate = true;
ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
return returnValue;
},
});
this._wrapperState.pendingUpdate = true;
ReactUpdates.asap(updateOptionsIfPendingUpdateAndMounted, this);
return returnValue;
}
module.exports = ReactDOMSelect;
@@ -323,9 +323,9 @@ describe('ReactDOMSelect', function() {
<option value="gorilla">A gorilla!</option>
</select>;
var markup = React.renderToString(stub);
expect(markup).toContain('<option value="giraffe" selected=');
expect(markup).not.toContain('<option value="monkey" selected=');
expect(markup).not.toContain('<option value="gorilla" selected=');
expect(markup).toContain('<option selected="" value="giraffe"');
expect(markup).not.toContain('<option selected="" value="monkey"');
expect(markup).not.toContain('<option selected="" value="gorilla"');
});
it('should support server-side rendering with defaultValue', function() {
@@ -336,9 +336,9 @@ describe('ReactDOMSelect', function() {
<option value="gorilla">A gorilla!</option>
</select>;
var markup = React.renderToString(stub);
expect(markup).toContain('<option value="giraffe" selected=');
expect(markup).not.toContain('<option value="monkey" selected=');
expect(markup).not.toContain('<option value="gorilla" selected=');
expect(markup).toContain('<option selected="" value="giraffe"');
expect(markup).not.toContain('<option selected="" value="monkey"');
expect(markup).not.toContain('<option selected="" value="gorilla"');
});
it('should support server-side rendering with multiple', function() {
@@ -349,9 +349,9 @@ describe('ReactDOMSelect', function() {
<option value="gorilla">A gorilla!</option>
</select>;
var markup = React.renderToString(stub);
expect(markup).toContain('<option value="giraffe" selected=');
expect(markup).toContain('<option value="gorilla" selected=');
expect(markup).not.toContain('<option value="monkey" selected=');
expect(markup).toContain('<option selected="" value="giraffe"');
expect(markup).toContain('<option selected="" value="gorilla"');
expect(markup).not.toContain('<option selected="" value="monkey"');
});
it('should not control defaultValue if readding options', function() {
+32 -2
View File
@@ -24,6 +24,8 @@ var ReactComponentBrowserEnvironment =
require('ReactComponentBrowserEnvironment');
var ReactDOMButton = require('ReactDOMButton');
var ReactDOMInput = require('ReactDOMInput');
var ReactDOMOption = require('ReactDOMOption');
var ReactDOMSelect = require('ReactDOMSelect');
var ReactDOMTextarea = require('ReactDOMTextarea');
var ReactMount = require('ReactMount');
var ReactMultiChild = require('ReactMultiChild');
@@ -229,6 +231,10 @@ function trapBubbledEventsLocal() {
}
}
function postUpdateSelectWrapper() {
ReactDOMSelect.postUpdateWrapper(this);
}
// For HTML, certain tags should omit their close tag. We keep a whitelist for
// those special cased tags.
@@ -350,11 +356,20 @@ ReactDOMComponent.Mixin = {
props = ReactDOMButton.getNativeProps(this, props, context);
break;
case 'input':
ReactDOMInput.mountWrapper(this, props);
ReactDOMInput.mountWrapper(this, props, context);
props = ReactDOMInput.getNativeProps(this, props, context);
break;
case 'option':
ReactDOMOption.mountWrapper(this, props, context);
props = ReactDOMOption.getNativeProps(this, props, context);
break;
case 'select':
ReactDOMSelect.mountWrapper(this, props, context);
props = ReactDOMSelect.getNativeProps(this, props, context);
context = ReactDOMSelect.processChildContext(this, props, context);
break;
case 'textarea':
ReactDOMTextarea.mountWrapper(this, props);
ReactDOMTextarea.mountWrapper(this, props, context);
props = ReactDOMTextarea.getNativeProps(this, props, context);
break;
}
@@ -376,6 +391,7 @@ ReactDOMComponent.Mixin = {
switch (this._tag) {
case 'button':
case 'input':
case 'select':
case 'textarea':
if (props.autoFocus) {
transaction.getReactMountReady().enqueue(
@@ -540,6 +556,14 @@ ReactDOMComponent.Mixin = {
lastProps = ReactDOMInput.getNativeProps(this, lastProps);
nextProps = ReactDOMInput.getNativeProps(this, nextProps);
break;
case 'option':
lastProps = ReactDOMOption.getNativeProps(this, lastProps);
nextProps = ReactDOMOption.getNativeProps(this, nextProps);
break;
case 'select':
lastProps = ReactDOMSelect.getNativeProps(this, lastProps);
nextProps = ReactDOMSelect.getNativeProps(this, nextProps);
break;
case 'textarea':
ReactDOMTextarea.updateWrapper(this);
lastProps = ReactDOMTextarea.getNativeProps(this, lastProps);
@@ -555,6 +579,12 @@ ReactDOMComponent.Mixin = {
transaction,
processChildContext(context, this)
);
if (this._tag === 'select') {
// <select> value update needs to occur after <option> children
// reconciliation
transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
}
},
/**
@@ -25,8 +25,6 @@ var ReactComponentBrowserEnvironment =
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
var ReactDOMComponent = require('ReactDOMComponent');
var ReactDOMIDOperations = require('ReactDOMIDOperations');
var ReactDOMOption = require('ReactDOMOption');
var ReactDOMSelect = require('ReactDOMSelect');
var ReactDOMTextComponent = require('ReactDOMTextComponent');
var ReactElement = require('ReactElement');
var ReactEventListener = require('ReactEventListener');
@@ -105,11 +103,6 @@ function inject() {
ReactInjection.Class.injectMixin(ReactBrowserComponentMixin);
ReactInjection.NativeComponent.injectComponentClasses({
'option': ReactDOMOption,
'select': ReactDOMSelect,
});
ReactInjection.DOMProperty.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
ReactInjection.DOMProperty.injectDOMPropertyConfig(SVGDOMPropertyConfig);