diff --git a/src/core/ReactMount.js b/src/core/ReactMount.js index cae78be541e0e..e8a21af2cd8b2 100644 --- a/src/core/ReactMount.js +++ b/src/core/ReactMount.js @@ -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]; diff --git a/src/dom/DefaultDOMPropertyConfig.js b/src/dom/DefaultDOMPropertyConfig.js index b068cc45dd446..6e6c4af9294ef 100644 --- a/src/dom/DefaultDOMPropertyConfig.js +++ b/src/dom/DefaultDOMPropertyConfig.js @@ -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, diff --git a/src/dom/components/ReactDOMInput.js b/src/dom/components/ReactDOMInput.js index 924a5768aeff2..4060943b2cf68 100644 --- a/src/dom/components/ReactDOMInput.js +++ b/src/dom/components/ReactDOMInput.js @@ -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 `ReactNativeComponent`. var input = ReactDOM.input; +var instancesByReactID = {}; + /** * Implements an native component that allows setting these optional * props: `checked`, `value`, `defaultChecked`, and `defaultValue`. @@ -71,6 +75,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( @@ -101,6 +116,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; } diff --git a/src/dom/components/__tests__/ReactDOMInput-test.js b/src/dom/components/__tests__/ReactDOMInput-test.js index 877b400080472..1b9f7d542f95f 100644 --- a/src/dom/components/__tests__/ReactDOMInput-test.js +++ b/src/dom/components/__tests__/ReactDOMInput-test.js @@ -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 ( +
+ A + B + +
+ +
+
+ ); + } + }); + + var stub = ReactTestUtils.renderIntoDocument(); + 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); + }); + });