mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #3595 from spicyj/select-ssr
Fix server-side rendering of <select>
This commit is contained in:
@@ -26,26 +26,26 @@ var hasReadOnlyValue = {
|
||||
'submit': true
|
||||
};
|
||||
|
||||
function _assertSingleLink(input) {
|
||||
function _assertSingleLink(inputProps) {
|
||||
invariant(
|
||||
input.props.checkedLink == null || input.props.valueLink == null,
|
||||
inputProps.checkedLink == null || inputProps.valueLink == null,
|
||||
'Cannot provide a checkedLink and a valueLink. If you want to use ' +
|
||||
'checkedLink, you probably don\'t want to use valueLink and vice versa.'
|
||||
);
|
||||
}
|
||||
function _assertValueLink(input) {
|
||||
_assertSingleLink(input);
|
||||
function _assertValueLink(inputProps) {
|
||||
_assertSingleLink(inputProps);
|
||||
invariant(
|
||||
input.props.value == null && input.props.onChange == null,
|
||||
inputProps.value == null && inputProps.onChange == null,
|
||||
'Cannot provide a valueLink and a value or onChange event. If you want ' +
|
||||
'to use value or onChange, you probably don\'t want to use valueLink.'
|
||||
);
|
||||
}
|
||||
|
||||
function _assertCheckedLink(input) {
|
||||
_assertSingleLink(input);
|
||||
function _assertCheckedLink(inputProps) {
|
||||
_assertSingleLink(inputProps);
|
||||
invariant(
|
||||
input.props.checked == null && input.props.onChange == null,
|
||||
inputProps.checked == null && inputProps.onChange == null,
|
||||
'Cannot provide a checkedLink and a checked property or onChange event. ' +
|
||||
'If you want to use checked or onChange, you probably don\'t want to ' +
|
||||
'use checkedLink'
|
||||
@@ -109,43 +109,43 @@ var LinkedValueUtils = {
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {ReactComponent} input Form component
|
||||
* @param {object} inputProps Props for form component
|
||||
* @return {*} current value of the input either from value prop or link.
|
||||
*/
|
||||
getValue: function(input) {
|
||||
if (input.props.valueLink) {
|
||||
_assertValueLink(input);
|
||||
return input.props.valueLink.value;
|
||||
getValue: function(inputProps) {
|
||||
if (inputProps.valueLink) {
|
||||
_assertValueLink(inputProps);
|
||||
return inputProps.valueLink.value;
|
||||
}
|
||||
return input.props.value;
|
||||
return inputProps.value;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {ReactComponent} input Form component
|
||||
* @param {object} inputProps Props for form component
|
||||
* @return {*} current checked status of the input either from checked prop
|
||||
* or link.
|
||||
*/
|
||||
getChecked: function(input) {
|
||||
if (input.props.checkedLink) {
|
||||
_assertCheckedLink(input);
|
||||
return input.props.checkedLink.value;
|
||||
getChecked: function(inputProps) {
|
||||
if (inputProps.checkedLink) {
|
||||
_assertCheckedLink(inputProps);
|
||||
return inputProps.checkedLink.value;
|
||||
}
|
||||
return input.props.checked;
|
||||
return inputProps.checked;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {ReactComponent} input Form component
|
||||
* @param {object} inputProps Props for form component
|
||||
* @return {function} change callback either from onChange prop or link.
|
||||
*/
|
||||
getOnChange: function(input) {
|
||||
if (input.props.valueLink) {
|
||||
_assertValueLink(input);
|
||||
getOnChange: function(inputProps) {
|
||||
if (inputProps.valueLink) {
|
||||
_assertValueLink(inputProps);
|
||||
return _handleLinkedValueChange;
|
||||
} else if (input.props.checkedLink) {
|
||||
_assertCheckedLink(input);
|
||||
} else if (inputProps.checkedLink) {
|
||||
_assertCheckedLink(inputProps);
|
||||
return _handleLinkedCheckChange;
|
||||
}
|
||||
return input.props.onChange;
|
||||
return inputProps.onChange;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -72,10 +72,10 @@ var ReactDOMInput = ReactClass.createClass({
|
||||
props.defaultChecked = null;
|
||||
props.defaultValue = null;
|
||||
|
||||
var value = LinkedValueUtils.getValue(this);
|
||||
var value = LinkedValueUtils.getValue(this.props);
|
||||
props.value = value != null ? value : this.state.initialValue;
|
||||
|
||||
var checked = LinkedValueUtils.getChecked(this);
|
||||
var checked = LinkedValueUtils.getChecked(this.props);
|
||||
props.checked = checked != null ? checked : this.state.initialChecked;
|
||||
|
||||
props.onChange = this._handleChange;
|
||||
@@ -104,7 +104,7 @@ var ReactDOMInput = ReactClass.createClass({
|
||||
);
|
||||
}
|
||||
|
||||
var value = LinkedValueUtils.getValue(this);
|
||||
var value = LinkedValueUtils.getValue(this.props);
|
||||
if (value != null) {
|
||||
// Cast `value` to a string to ensure the value is set correctly. While
|
||||
// browsers typically do this as necessary, jsdom doesn't.
|
||||
@@ -114,7 +114,7 @@ var ReactDOMInput = ReactClass.createClass({
|
||||
|
||||
_handleChange: function(event) {
|
||||
var returnValue;
|
||||
var onChange = LinkedValueUtils.getOnChange(this);
|
||||
var onChange = LinkedValueUtils.getOnChange(this.props);
|
||||
if (onChange) {
|
||||
returnValue = onChange.call(this, event);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,18 @@
|
||||
|
||||
var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
|
||||
var ReactClass = require('ReactClass');
|
||||
var ReactDOMSelect = require('ReactDOMSelect');
|
||||
var ReactElement = require('ReactElement');
|
||||
var ReactInstanceMap = require('ReactInstanceMap');
|
||||
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.
|
||||
*/
|
||||
@@ -28,6 +34,16 @@ var ReactDOMOption = ReactClass.createClass({
|
||||
|
||||
mixins: [ReactBrowserComponentMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {selected: null};
|
||||
},
|
||||
|
||||
contextTypes: (function() {
|
||||
var obj = {};
|
||||
obj[valueContextKey] = ReactPropTypes.any;
|
||||
return obj;
|
||||
})(),
|
||||
|
||||
componentWillMount: function() {
|
||||
// TODO (yungsters): Remove support for `selected` in <option>.
|
||||
if (__DEV__) {
|
||||
@@ -37,10 +53,40 @@ var ReactDOMOption = ReactClass.createClass({
|
||||
'setting `selected` on <option>.'
|
||||
);
|
||||
}
|
||||
|
||||
// Look up whether this option is 'selected' via parent-based context
|
||||
var context = ReactInstanceMap.get(this)._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
|
||||
if (selectValue != null) {
|
||||
var selected = false;
|
||||
if (Array.isArray(selectValue)) {
|
||||
// multiple
|
||||
for (var i = 0; i < selectValue.length; i++) {
|
||||
if ('' + selectValue[i] === '' + this.props.value) {
|
||||
selected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selected = ('' + selectValue === '' + this.props.value);
|
||||
}
|
||||
this.setState({selected: selected});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return option(this.props, this.props.children);
|
||||
var props = this.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});
|
||||
}
|
||||
|
||||
return option(props, this.props.children);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -17,17 +17,21 @@ var ReactBrowserComponentMixin = require('ReactBrowserComponentMixin');
|
||||
var ReactClass = require('ReactClass');
|
||||
var ReactElement = require('ReactElement');
|
||||
var ReactUpdates = require('ReactUpdates');
|
||||
var ReactPropTypes = require('ReactPropTypes');
|
||||
|
||||
var assign = require('Object.assign');
|
||||
var findDOMNode = require('findDOMNode');
|
||||
|
||||
var select = ReactElement.createFactory('select');
|
||||
|
||||
var valueContextKey =
|
||||
'__ReactDOMSelect_value$' + Math.random().toString(36).slice(2);
|
||||
|
||||
function updateOptionsIfPendingUpdateAndMounted() {
|
||||
/*jshint validthis:true */
|
||||
if (this._pendingUpdate) {
|
||||
this._pendingUpdate = false;
|
||||
var value = LinkedValueUtils.getValue(this);
|
||||
var value = LinkedValueUtils.getValue(this.props);
|
||||
if (value != null && this.isMounted()) {
|
||||
updateOptions(this, value);
|
||||
}
|
||||
@@ -116,11 +120,38 @@ var ReactDOMSelect = ReactClass.createClass({
|
||||
|
||||
mixins: [AutoFocusMixin, LinkedValueUtils.Mixin, ReactBrowserComponentMixin],
|
||||
|
||||
statics: {
|
||||
valueContextKey: valueContextKey
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
defaultValue: selectValueType,
|
||||
value: selectValueType
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
// 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};
|
||||
}
|
||||
},
|
||||
|
||||
childContextTypes: (function() {
|
||||
var obj = {};
|
||||
obj[valueContextKey] = ReactPropTypes.any;
|
||||
return obj;
|
||||
})(),
|
||||
|
||||
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);
|
||||
@@ -135,17 +166,14 @@ var ReactDOMSelect = ReactClass.createClass({
|
||||
this._pendingUpdate = false;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var value = LinkedValueUtils.getValue(this);
|
||||
if (value != null) {
|
||||
updateOptions(this, value);
|
||||
} else if (this.props.defaultValue != null) {
|
||||
updateOptions(this, this.props.defaultValue);
|
||||
}
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// After the initial mount, we control selected-ness manually so don't pass
|
||||
// the context value down
|
||||
this.setState({initialValue: null});
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps) {
|
||||
var value = LinkedValueUtils.getValue(this);
|
||||
var value = LinkedValueUtils.getValue(this.props);
|
||||
if (value != null) {
|
||||
this._pendingUpdate = false;
|
||||
updateOptions(this, value);
|
||||
@@ -162,7 +190,7 @@ var ReactDOMSelect = ReactClass.createClass({
|
||||
|
||||
_handleChange: function(event) {
|
||||
var returnValue;
|
||||
var onChange = LinkedValueUtils.getOnChange(this);
|
||||
var onChange = LinkedValueUtils.getOnChange(this.props);
|
||||
if (onChange) {
|
||||
returnValue = onChange.call(this, event);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ var ReactDOMTextarea = ReactClass.createClass({
|
||||
if (defaultValue == null) {
|
||||
defaultValue = '';
|
||||
}
|
||||
var value = LinkedValueUtils.getValue(this);
|
||||
var value = LinkedValueUtils.getValue(this.props);
|
||||
return {
|
||||
// We save the initial value so that `ReactDOMComponent` doesn't update
|
||||
// `textContent` (unnecessary since we update value).
|
||||
@@ -113,7 +113,7 @@ var ReactDOMTextarea = ReactClass.createClass({
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState, prevContext) {
|
||||
var value = LinkedValueUtils.getValue(this);
|
||||
var value = LinkedValueUtils.getValue(this.props);
|
||||
if (value != null) {
|
||||
var rootNode = findDOMNode(this);
|
||||
// Cast `value` to a string to ensure the value is set correctly. While
|
||||
@@ -124,7 +124,7 @@ var ReactDOMTextarea = ReactClass.createClass({
|
||||
|
||||
_handleChange: function(event) {
|
||||
var returnValue;
|
||||
var onChange = LinkedValueUtils.getOnChange(this);
|
||||
var onChange = LinkedValueUtils.getOnChange(this.props);
|
||||
if (onChange) {
|
||||
returnValue = onChange.call(this, event);
|
||||
}
|
||||
|
||||
@@ -315,4 +315,85 @@ describe('ReactDOMSelect', function() {
|
||||
expect(link.requestChange.mock.calls[0][0]).toEqual('gorilla');
|
||||
|
||||
});
|
||||
|
||||
it('should support server-side rendering', function() {
|
||||
var stub =
|
||||
<select value="giraffe">
|
||||
<option value="monkey">A monkey!</option>
|
||||
<option value="giraffe">A giraffe!</option>
|
||||
<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=');
|
||||
});
|
||||
|
||||
it('should support server-side rendering with defaultValue', function() {
|
||||
var stub =
|
||||
<select defaultValue="giraffe">
|
||||
<option value="monkey">A monkey!</option>
|
||||
<option value="giraffe">A giraffe!</option>
|
||||
<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=');
|
||||
});
|
||||
|
||||
it('should support server-side rendering with multiple', function() {
|
||||
var stub =
|
||||
<select multiple={true} value={['giraffe', 'gorilla']}>
|
||||
<option value="monkey">A monkey!</option>
|
||||
<option value="giraffe">A giraffe!</option>
|
||||
<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=');
|
||||
});
|
||||
|
||||
it('should not control defaultValue if readding options', function() {
|
||||
var container = document.createElement('div');
|
||||
|
||||
var select = React.render(
|
||||
<select multiple={true} defaultValue={['giraffe']}>
|
||||
<option key="monkey" value="monkey">A monkey!</option>
|
||||
<option key="giraffe" value="giraffe">A giraffe!</option>
|
||||
<option key="gorilla" value="gorilla">A gorilla!</option>
|
||||
</select>,
|
||||
container
|
||||
);
|
||||
var node = React.findDOMNode(select);
|
||||
|
||||
expect(node.options[0].selected).toBe(false); // monkey
|
||||
expect(node.options[1].selected).toBe(true); // giraffe
|
||||
expect(node.options[2].selected).toBe(false); // gorilla
|
||||
|
||||
React.render(
|
||||
<select multiple={true} defaultValue={['giraffe']}>
|
||||
<option key="monkey" value="monkey">A monkey!</option>
|
||||
<option key="gorilla" value="gorilla">A gorilla!</option>
|
||||
</select>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(node.options[0].selected).toBe(false); // monkey
|
||||
expect(node.options[1].selected).toBe(false); // gorilla
|
||||
|
||||
React.render(
|
||||
<select multiple={true} defaultValue={['giraffe']}>
|
||||
<option key="monkey" value="monkey">A monkey!</option>
|
||||
<option key="giraffe" value="giraffe">A giraffe!</option>
|
||||
<option key="gorilla" value="gorilla">A gorilla!</option>
|
||||
</select>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(node.options[0].selected).toBe(false); // monkey
|
||||
expect(node.options[1].selected).toBe(false); // giraffe
|
||||
expect(node.options[2].selected).toBe(false); // gorilla
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user