mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #4035 from spicyj/dc-os
Convert select/option to not use wrappers
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user