Merge pull request #281 from spicyj/radio

Fix controlled radio button behavior
This commit is contained in:
Paul O’Shannessy
2013-09-07 13:54:57 -07:00
4 changed files with 105 additions and 6 deletions
+6 -6
View File
@@ -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];
+1
View File
@@ -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,
+55
View File
@@ -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);
});
});