Merge pull request #3595 from spicyj/select-ssr

Fix server-side rendering of <select>
This commit is contained in:
Ben Alpert
2015-04-07 16:49:10 -07:00
6 changed files with 200 additions and 45 deletions
@@ -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);
}
});
+38 -10
View File
@@ -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
});
});