284 lines
13 KiB
JavaScript
284 lines
13 KiB
JavaScript
|
/**
|
||
|
* Copyright 2013-present, Facebook, Inc.
|
||
|
* All rights reserved.
|
||
|
*
|
||
|
* This source code is licensed under the BSD-style license found in the
|
||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var _prodInvariant = require('./reactProdInvariant'),
|
||
|
_assign = require('object-assign');
|
||
|
|
||
|
var DOMPropertyOperations = require('./DOMPropertyOperations');
|
||
|
var LinkedValueUtils = require('./LinkedValueUtils');
|
||
|
var ReactDOMComponentTree = require('./ReactDOMComponentTree');
|
||
|
var ReactUpdates = require('./ReactUpdates');
|
||
|
|
||
|
var invariant = require('fbjs/lib/invariant');
|
||
|
var warning = require('fbjs/lib/warning');
|
||
|
|
||
|
var didWarnValueLink = false;
|
||
|
var didWarnCheckedLink = false;
|
||
|
var didWarnValueDefaultValue = false;
|
||
|
var didWarnCheckedDefaultChecked = false;
|
||
|
var didWarnControlledToUncontrolled = false;
|
||
|
var didWarnUncontrolledToControlled = false;
|
||
|
|
||
|
function forceUpdateIfMounted() {
|
||
|
if (this._rootNodeID) {
|
||
|
// DOM component is still mounted; update
|
||
|
ReactDOMInput.updateWrapper(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function isControlled(props) {
|
||
|
var usesChecked = props.type === 'checkbox' || props.type === 'radio';
|
||
|
return usesChecked ? props.checked != null : props.value != null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Implements an <input> host 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 = {
|
||
|
getHostProps: function (inst, props) {
|
||
|
var value = LinkedValueUtils.getValue(props);
|
||
|
var checked = LinkedValueUtils.getChecked(props);
|
||
|
|
||
|
var hostProps = _assign({
|
||
|
// Make sure we set .type before any other properties (setting .value
|
||
|
// before .type means .value is lost in IE11 and below)
|
||
|
type: undefined,
|
||
|
// Make sure we set .step before .value (setting .value before .step
|
||
|
// means .value is rounded on mount, based upon step precision)
|
||
|
step: undefined,
|
||
|
// Make sure we set .min & .max before .value (to ensure proper order
|
||
|
// in corner cases such as min or max deriving from value, e.g. Issue #7170)
|
||
|
min: undefined,
|
||
|
max: undefined
|
||
|
}, props, {
|
||
|
defaultChecked: undefined,
|
||
|
defaultValue: undefined,
|
||
|
value: value != null ? value : inst._wrapperState.initialValue,
|
||
|
checked: checked != null ? checked : inst._wrapperState.initialChecked,
|
||
|
onChange: inst._wrapperState.onChange
|
||
|
});
|
||
|
|
||
|
return hostProps;
|
||
|
},
|
||
|
|
||
|
mountWrapper: function (inst, props) {
|
||
|
if (process.env.NODE_ENV !== 'production') {
|
||
|
LinkedValueUtils.checkPropTypes('input', props, inst._currentElement._owner);
|
||
|
|
||
|
var owner = inst._currentElement._owner;
|
||
|
|
||
|
if (props.valueLink !== undefined && !didWarnValueLink) {
|
||
|
process.env.NODE_ENV !== 'production' ? warning(false, '`valueLink` prop on `input` is deprecated; set `value` and `onChange` instead.') : void 0;
|
||
|
didWarnValueLink = true;
|
||
|
}
|
||
|
if (props.checkedLink !== undefined && !didWarnCheckedLink) {
|
||
|
process.env.NODE_ENV !== 'production' ? warning(false, '`checkedLink` prop on `input` is deprecated; set `value` and `onChange` instead.') : void 0;
|
||
|
didWarnCheckedLink = true;
|
||
|
}
|
||
|
if (props.checked !== undefined && props.defaultChecked !== undefined && !didWarnCheckedDefaultChecked) {
|
||
|
process.env.NODE_ENV !== 'production' ? warning(false, '%s contains an input of type %s with both checked and defaultChecked props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the checked prop, or the defaultChecked prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', owner && owner.getName() || 'A component', props.type) : void 0;
|
||
|
didWarnCheckedDefaultChecked = true;
|
||
|
}
|
||
|
if (props.value !== undefined && props.defaultValue !== undefined && !didWarnValueDefaultValue) {
|
||
|
process.env.NODE_ENV !== 'production' ? warning(false, '%s contains an input of type %s with both value and defaultValue props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', owner && owner.getName() || 'A component', props.type) : void 0;
|
||
|
didWarnValueDefaultValue = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var defaultValue = props.defaultValue;
|
||
|
inst._wrapperState = {
|
||
|
initialChecked: props.checked != null ? props.checked : props.defaultChecked,
|
||
|
initialValue: props.value != null ? props.value : defaultValue,
|
||
|
listeners: null,
|
||
|
onChange: _handleChange.bind(inst),
|
||
|
controlled: isControlled(props)
|
||
|
};
|
||
|
},
|
||
|
|
||
|
updateWrapper: function (inst) {
|
||
|
var props = inst._currentElement.props;
|
||
|
|
||
|
if (process.env.NODE_ENV !== 'production') {
|
||
|
var controlled = isControlled(props);
|
||
|
var owner = inst._currentElement._owner;
|
||
|
|
||
|
if (!inst._wrapperState.controlled && controlled && !didWarnUncontrolledToControlled) {
|
||
|
process.env.NODE_ENV !== 'production' ? warning(false, '%s is changing an uncontrolled input of type %s to be controlled. ' + 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', owner && owner.getName() || 'A component', props.type) : void 0;
|
||
|
didWarnUncontrolledToControlled = true;
|
||
|
}
|
||
|
if (inst._wrapperState.controlled && !controlled && !didWarnControlledToUncontrolled) {
|
||
|
process.env.NODE_ENV !== 'production' ? warning(false, '%s is changing a controlled input of type %s to be uncontrolled. ' + 'Input elements should not switch from controlled to uncontrolled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', owner && owner.getName() || 'A component', props.type) : void 0;
|
||
|
didWarnControlledToUncontrolled = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO: Shouldn't this be getChecked(props)?
|
||
|
var checked = props.checked;
|
||
|
if (checked != null) {
|
||
|
DOMPropertyOperations.setValueForProperty(ReactDOMComponentTree.getNodeFromInstance(inst), 'checked', checked || false);
|
||
|
}
|
||
|
|
||
|
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
|
||
|
var value = LinkedValueUtils.getValue(props);
|
||
|
if (value != null) {
|
||
|
if (value === 0 && node.value === '') {
|
||
|
node.value = '0';
|
||
|
// Note: IE9 reports a number inputs as 'text', so check props instead.
|
||
|
} else if (props.type === 'number') {
|
||
|
// Simulate `input.valueAsNumber`. IE9 does not support it
|
||
|
var valueAsNumber = parseFloat(node.value, 10) || 0;
|
||
|
|
||
|
// eslint-disable-next-line
|
||
|
if (value != valueAsNumber) {
|
||
|
// Cast `value` to a string to ensure the value is set correctly. While
|
||
|
// browsers typically do this as necessary, jsdom doesn't.
|
||
|
node.value = '' + value;
|
||
|
}
|
||
|
// eslint-disable-next-line
|
||
|
} else if (value != node.value) {
|
||
|
// Cast `value` to a string to ensure the value is set correctly. While
|
||
|
// browsers typically do this as necessary, jsdom doesn't.
|
||
|
node.value = '' + value;
|
||
|
}
|
||
|
} else {
|
||
|
if (props.value == null && props.defaultValue != null) {
|
||
|
// In Chrome, assigning defaultValue to certain input types triggers input validation.
|
||
|
// For number inputs, the display value loses trailing decimal points. For email inputs,
|
||
|
// Chrome raises "The specified value <x> is not a valid email address".
|
||
|
//
|
||
|
// Here we check to see if the defaultValue has actually changed, avoiding these problems
|
||
|
// when the user is inputting text
|
||
|
//
|
||
|
// https://github.com/facebook/react/issues/7253
|
||
|
if (node.defaultValue !== '' + props.defaultValue) {
|
||
|
node.defaultValue = '' + props.defaultValue;
|
||
|
}
|
||
|
}
|
||
|
if (props.checked == null && props.defaultChecked != null) {
|
||
|
node.defaultChecked = !!props.defaultChecked;
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
postMountWrapper: function (inst) {
|
||
|
var props = inst._currentElement.props;
|
||
|
|
||
|
// This is in postMount because we need access to the DOM node, which is not
|
||
|
// available until after the component has mounted.
|
||
|
var node = ReactDOMComponentTree.getNodeFromInstance(inst);
|
||
|
|
||
|
// Detach value from defaultValue. We won't do anything if we're working on
|
||
|
// submit or reset inputs as those values & defaultValues are linked. They
|
||
|
// are not resetable nodes so this operation doesn't matter and actually
|
||
|
// removes browser-default values (eg "Submit Query") when no value is
|
||
|
// provided.
|
||
|
|
||
|
switch (props.type) {
|
||
|
case 'submit':
|
||
|
case 'reset':
|
||
|
break;
|
||
|
case 'color':
|
||
|
case 'date':
|
||
|
case 'datetime':
|
||
|
case 'datetime-local':
|
||
|
case 'month':
|
||
|
case 'time':
|
||
|
case 'week':
|
||
|
// This fixes the no-show issue on iOS Safari and Android Chrome:
|
||
|
// https://github.com/facebook/react/issues/7233
|
||
|
node.value = '';
|
||
|
node.value = node.defaultValue;
|
||
|
break;
|
||
|
default:
|
||
|
node.value = node.value;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
|
||
|
// this is needed to work around a chrome bug where setting defaultChecked
|
||
|
// will sometimes influence the value of checked (even after detachment).
|
||
|
// Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
|
||
|
// We need to temporarily unset name to avoid disrupting radio button groups.
|
||
|
var name = node.name;
|
||
|
if (name !== '') {
|
||
|
node.name = '';
|
||
|
}
|
||
|
node.defaultChecked = !node.defaultChecked;
|
||
|
node.defaultChecked = !node.defaultChecked;
|
||
|
if (name !== '') {
|
||
|
node.name = name;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function _handleChange(event) {
|
||
|
var props = this._currentElement.props;
|
||
|
|
||
|
var returnValue = LinkedValueUtils.executeOnChange(props, event);
|
||
|
|
||
|
// Here we use asap to wait until all updates have propagated, which
|
||
|
// is important when using controlled components within layers:
|
||
|
// https://github.com/facebook/react/issues/1698
|
||
|
ReactUpdates.asap(forceUpdateIfMounted, this);
|
||
|
|
||
|
var name = props.name;
|
||
|
if (props.type === 'radio' && name != null) {
|
||
|
var rootNode = ReactDOMComponentTree.getNodeFromInstance(this);
|
||
|
var queryRoot = rootNode;
|
||
|
|
||
|
while (queryRoot.parentNode) {
|
||
|
queryRoot = queryRoot.parentNode;
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
// `querySelectorAll` to ensure we don't miss anything.
|
||
|
var group = queryRoot.querySelectorAll('input[name=' + JSON.stringify('' + name) + '][type="radio"]');
|
||
|
|
||
|
for (var i = 0; i < group.length; i++) {
|
||
|
var otherNode = group[i];
|
||
|
if (otherNode === rootNode || otherNode.form !== rootNode.form) {
|
||
|
continue;
|
||
|
}
|
||
|
// This will throw if radio buttons rendered by different copies of React
|
||
|
// and the same name are rendered into the same form (same as #1939).
|
||
|
// That's probably okay; we don't support it just as we don't support
|
||
|
// mixing React radio buttons with non-React ones.
|
||
|
var otherInstance = ReactDOMComponentTree.getInstanceFromNode(otherNode);
|
||
|
!otherInstance ? process.env.NODE_ENV !== 'production' ? invariant(false, 'ReactDOMInput: Mixing React and non-React radio inputs with the same `name` is not supported.') : _prodInvariant('90') : void 0;
|
||
|
// If this is a controlled radio button group, forcing the input that
|
||
|
// was previously checked to update will cause it to be come re-checked
|
||
|
// as appropriate.
|
||
|
ReactUpdates.asap(forceUpdateIfMounted, otherInstance);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return returnValue;
|
||
|
}
|
||
|
|
||
|
module.exports = ReactDOMInput;
|