mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #281 from spicyj/radio
Fix controlled radio button behavior
This commit is contained in:
@@ -31,7 +31,7 @@ var nodeCache = {};
|
||||
var $ = require('$');
|
||||
|
||||
/** Mapping from reactRootID to React component instance. */
|
||||
var instanceByReactRootID = {};
|
||||
var instancesByReactRootID = {};
|
||||
|
||||
/** Mapping from reactRootID to `container` nodes. */
|
||||
var containersByReactRootID = {};
|
||||
@@ -212,7 +212,7 @@ var ReactMount = {
|
||||
useTouchEvents: false,
|
||||
|
||||
/** Exposed for debugging purposes **/
|
||||
_instanceByReactRootID: instanceByReactRootID,
|
||||
_instancesByReactRootID: instancesByReactRootID,
|
||||
|
||||
/**
|
||||
* This is a hook provided to support rendering React components while
|
||||
@@ -272,7 +272,7 @@ var ReactMount = {
|
||||
ReactMount.prepareEnvironmentForDOM();
|
||||
|
||||
var reactRootID = ReactMount.registerContainer(container);
|
||||
instanceByReactRootID[reactRootID] = nextComponent;
|
||||
instancesByReactRootID[reactRootID] = nextComponent;
|
||||
return reactRootID;
|
||||
},
|
||||
|
||||
@@ -316,7 +316,7 @@ var ReactMount = {
|
||||
* @return {ReactComponent} Component instance rendered in `container`.
|
||||
*/
|
||||
renderComponent: function(nextComponent, container, callback) {
|
||||
var registeredComponent = instanceByReactRootID[getReactRootID(container)];
|
||||
var registeredComponent = instancesByReactRootID[getReactRootID(container)];
|
||||
|
||||
if (registeredComponent) {
|
||||
if (registeredComponent.constructor === nextComponent.constructor) {
|
||||
@@ -403,12 +403,12 @@ var ReactMount = {
|
||||
*/
|
||||
unmountAndReleaseReactRootNode: function(container) {
|
||||
var reactRootID = getReactRootID(container);
|
||||
var component = instanceByReactRootID[reactRootID];
|
||||
var component = instancesByReactRootID[reactRootID];
|
||||
if (!component) {
|
||||
return false;
|
||||
}
|
||||
ReactMount.unmountComponentFromNode(component, container);
|
||||
delete instanceByReactRootID[reactRootID];
|
||||
delete instancesByReactRootID[reactRootID];
|
||||
delete containersByReactRootID[reactRootID];
|
||||
if (__DEV__) {
|
||||
delete rootElementsByReactRootID[reactRootID];
|
||||
|
||||
@@ -59,6 +59,7 @@ var DefaultDOMPropertyConfig = {
|
||||
disabled: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
|
||||
draggable: null,
|
||||
encType: null,
|
||||
form: MUST_USE_ATTRIBUTE,
|
||||
frameBorder: MUST_USE_ATTRIBUTE,
|
||||
height: MUST_USE_ATTRIBUTE,
|
||||
hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
|
||||
|
||||
@@ -21,12 +21,16 @@
|
||||
var DOMPropertyOperations = require('DOMPropertyOperations');
|
||||
var ReactCompositeComponent = require('ReactCompositeComponent');
|
||||
var ReactDOM = require('ReactDOM');
|
||||
var ReactMount = require('ReactMount');
|
||||
|
||||
var invariant = require('invariant');
|
||||
var merge = require('merge');
|
||||
|
||||
// Store a reference to the <input> `ReactNativeComponent`.
|
||||
var input = ReactDOM.input;
|
||||
|
||||
var instancesByReactID = {};
|
||||
|
||||
/**
|
||||
* Implements an <input> native component that allows setting these optional
|
||||
* props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
|
||||
@@ -73,6 +77,17 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
|
||||
return input(props, this.props.children);
|
||||
},
|
||||
|
||||
componentDidMount: function(rootNode) {
|
||||
var id = ReactMount.getID(rootNode);
|
||||
instancesByReactID[id] = this;
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
var rootNode = this.getDOMNode();
|
||||
var id = ReactMount.getID(rootNode);
|
||||
delete instancesByReactID[id];
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState, rootNode) {
|
||||
if (this.props.checked != null) {
|
||||
DOMPropertyOperations.setValueForProperty(
|
||||
@@ -103,6 +118,46 @@ var ReactDOMInput = ReactCompositeComponent.createClass({
|
||||
checked: event.target.checked,
|
||||
value: event.target.value
|
||||
});
|
||||
|
||||
var name = this.props.name;
|
||||
if (this.props.type === 'radio' && name != null) {
|
||||
var rootNode = this.getDOMNode();
|
||||
// If `rootNode.form` was non-null, then we could try `form.elements`,
|
||||
// but that sometimes behaves strangely in IE8. We could also try using
|
||||
// `form.getElementsByName`, but that will only return direct children
|
||||
// and won't include inputs that use the HTML5 `form=` attribute. Since
|
||||
// the input might not even be in a form, let's just use the global
|
||||
// `getElementsByName` to ensure we don't miss anything.
|
||||
var group = document.getElementsByName(name);
|
||||
for (var i = 0; i < group.length; i++) {
|
||||
var otherNode = group[i];
|
||||
if (otherNode === rootNode ||
|
||||
otherNode.nodeName !== 'INPUT' || otherNode.type !== 'radio' ||
|
||||
otherNode.form !== rootNode.form) {
|
||||
continue;
|
||||
}
|
||||
var otherID = ReactMount.getID(otherNode);
|
||||
invariant(
|
||||
otherID,
|
||||
'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
|
||||
'same `name` is not supported.'
|
||||
);
|
||||
var otherInstance = instancesByReactID[otherID];
|
||||
invariant(
|
||||
otherInstance,
|
||||
'ReactDOMInput: Unknown radio button ID %s.',
|
||||
otherID
|
||||
);
|
||||
// In some cases, this will actually change the `checked` state value.
|
||||
// In other cases, there's no change but this forces a reconcile upon
|
||||
// which componentDidUpdate will reset the DOM property to whatever it
|
||||
// should be.
|
||||
otherInstance.setState({
|
||||
checked: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,4 +52,47 @@ describe('ReactDOMInput', function() {
|
||||
expect(node.value).toBe('0');
|
||||
});
|
||||
|
||||
it('should control radio buttons', function() {
|
||||
var RadioGroup = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<input ref="a" type="radio" name="fruit" checked={true} />A
|
||||
<input ref="b" type="radio" name="fruit" />B
|
||||
|
||||
<form>
|
||||
<input ref="c" type="radio" name="fruit" form="pluto"
|
||||
defaultChecked={true} />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var stub = ReactTestUtils.renderIntoDocument(<RadioGroup />);
|
||||
var aNode = stub.refs.a.getDOMNode();
|
||||
var bNode = stub.refs.b.getDOMNode();
|
||||
var cNode = stub.refs.c.getDOMNode();
|
||||
|
||||
expect(aNode.checked).toBe(true);
|
||||
expect(bNode.checked).toBe(false);
|
||||
// c is in a separate form and shouldn't be affected at all here
|
||||
expect(cNode.checked).toBe(true);
|
||||
|
||||
bNode.checked = true;
|
||||
// This next line isn't necessary in a proper browser environment, but
|
||||
// jsdom doesn't uncheck the others in a group (which makes this whole test
|
||||
// a little less effective)
|
||||
aNode.checked = false;
|
||||
expect(cNode.checked).toBe(true);
|
||||
|
||||
// Now let's run the actual ReactDOMInput change event handler (on radio
|
||||
// inputs, ChangeEventPlugin listens for the `click` event so trigger that)
|
||||
ReactTestUtils.Simulate.click(bNode);
|
||||
|
||||
// The original state should have been restored
|
||||
expect(aNode.checked).toBe(true);
|
||||
expect(cNode.checked).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user