/** * Copyright 2013 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * @providesModule ReactDOMInput */ "use strict"; var DOMPropertyOperations = require('DOMPropertyOperations'); var LinkedValueMixin = require('LinkedValueMixin'); 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 `ReactDOMComponent`. var input = ReactDOM.input; var instancesByReactID = {}; /** * Implements an native component that allows setting these optional * props: `checked`, `value`, `defaultChecked`, and `defaultValue`. * * If `checked` or `value` are not supplied (or null/undefined), user actions * that affect the checked state or value will trigger updates to the element. * * If they are supplied (and not null/undefined), the rendered element will not * trigger updates to the element. Instead, the props must change in order for * the rendered element to be updated. * * The rendered element will be initialized as unchecked (or `defaultChecked`) * with an empty value (or `defaultValue`). * * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html */ var ReactDOMInput = ReactCompositeComponent.createClass({ mixins: [LinkedValueMixin], getInitialState: function() { var defaultValue = this.props.defaultValue; return { checked: this.props.defaultChecked || false, value: defaultValue != null ? defaultValue : null }; }, shouldComponentUpdate: function() { // Defer any updates to this component during the `onChange` handler. return !this._isChanging; }, render: function() { // Clone `this.props` so we don't mutate the input. var props = merge(this.props); props.defaultChecked = null; props.defaultValue = null; props.checked = this.props.checked != null ? this.props.checked : this.state.checked; var value = this.getValue(); props.value = value != null ? value : this.state.value; props.onChange = this._handleChange; 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( rootNode, 'checked', this.props.checked || false ); } var value = this.getValue(); 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. DOMPropertyOperations.setValueForProperty(rootNode, 'value', '' + value); } }, _handleChange: function(event) { var returnValue; var onChange = this.getOnChange(); if (onChange) { this._isChanging = true; returnValue = onChange(event); this._isChanging = false; } this.setState({ 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, groupLen = group.length; i < groupLen; 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; } }); module.exports = ReactDOMInput;