UNPKG

13 kBJavaScriptView Raw
1/**
2 * Copyright (c) 2013-present, Facebook, Inc.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 */
8
9'use strict';
10
11var _prodInvariant = require('./reactProdInvariant'),
12 _assign = require('object-assign');
13
14var DOMPropertyOperations = require('./DOMPropertyOperations');
15var LinkedValueUtils = require('./LinkedValueUtils');
16var ReactDOMComponentTree = require('./ReactDOMComponentTree');
17var ReactUpdates = require('./ReactUpdates');
18
19var invariant = require('fbjs/lib/invariant');
20var warning = require('fbjs/lib/warning');
21
22var didWarnValueLink = false;
23var didWarnCheckedLink = false;
24var didWarnValueDefaultValue = false;
25var didWarnCheckedDefaultChecked = false;
26var didWarnControlledToUncontrolled = false;
27var didWarnUncontrolledToControlled = false;
28
29function forceUpdateIfMounted() {
30 if (this._rootNodeID) {
31 // DOM component is still mounted; update
32 ReactDOMInput.updateWrapper(this);
33 }
34}
35
36function isControlled(props) {
37 var usesChecked = props.type === 'checkbox' || props.type === 'radio';
38 return usesChecked ? props.checked != null : props.value != null;
39}
40
41/**
42 * Implements an <input> host component that allows setting these optional
43 * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
44 *
45 * If `checked` or `value` are not supplied (or null/undefined), user actions
46 * that affect the checked state or value will trigger updates to the element.
47 *
48 * If they are supplied (and not null/undefined), the rendered element will not
49 * trigger updates to the element. Instead, the props must change in order for
50 * the rendered element to be updated.
51 *
52 * The rendered element will be initialized as unchecked (or `defaultChecked`)
53 * with an empty value (or `defaultValue`).
54 *
55 * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
56 */
57var ReactDOMInput = {
58 getHostProps: function (inst, props) {
59 var value = LinkedValueUtils.getValue(props);
60 var checked = LinkedValueUtils.getChecked(props);
61
62 var hostProps = _assign({
63 // Make sure we set .type before any other properties (setting .value
64 // before .type means .value is lost in IE11 and below)
65 type: undefined,
66 // Make sure we set .step before .value (setting .value before .step
67 // means .value is rounded on mount, based upon step precision)
68 step: undefined,
69 // Make sure we set .min & .max before .value (to ensure proper order
70 // in corner cases such as min or max deriving from value, e.g. Issue #7170)
71 min: undefined,
72 max: undefined
73 }, props, {
74 defaultChecked: undefined,
75 defaultValue: undefined,
76 value: value != null ? value : inst._wrapperState.initialValue,
77 checked: checked != null ? checked : inst._wrapperState.initialChecked,
78 onChange: inst._wrapperState.onChange
79 });
80
81 return hostProps;
82 },
83
84 mountWrapper: function (inst, props) {
85 if (process.env.NODE_ENV !== 'production') {
86 LinkedValueUtils.checkPropTypes('input', props, inst._currentElement._owner);
87
88 var owner = inst._currentElement._owner;
89
90 if (props.valueLink !== undefined && !didWarnValueLink) {
91 process.env.NODE_ENV !== 'production' ? warning(false, '`valueLink` prop on `input` is deprecated; set `value` and `onChange` instead.') : void 0;
92 didWarnValueLink = true;
93 }
94 if (props.checkedLink !== undefined && !didWarnCheckedLink) {
95 process.env.NODE_ENV !== 'production' ? warning(false, '`checkedLink` prop on `input` is deprecated; set `value` and `onChange` instead.') : void 0;
96 didWarnCheckedLink = true;
97 }
98 if (props.checked !== undefined && props.defaultChecked !== undefined && !didWarnCheckedDefaultChecked) {
99 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;
100 didWarnCheckedDefaultChecked = true;
101 }
102 if (props.value !== undefined && props.defaultValue !== undefined && !didWarnValueDefaultValue) {
103 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;
104 didWarnValueDefaultValue = true;
105 }
106 }
107
108 var defaultValue = props.defaultValue;
109 inst._wrapperState = {
110 initialChecked: props.checked != null ? props.checked : props.defaultChecked,
111 initialValue: props.value != null ? props.value : defaultValue,
112 listeners: null,
113 onChange: _handleChange.bind(inst),
114 controlled: isControlled(props)
115 };
116 },
117
118 updateWrapper: function (inst) {
119 var props = inst._currentElement.props;
120
121 if (process.env.NODE_ENV !== 'production') {
122 var controlled = isControlled(props);
123 var owner = inst._currentElement._owner;
124
125 if (!inst._wrapperState.controlled && controlled && !didWarnUncontrolledToControlled) {
126 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;
127 didWarnUncontrolledToControlled = true;
128 }
129 if (inst._wrapperState.controlled && !controlled && !didWarnControlledToUncontrolled) {
130 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;
131 didWarnControlledToUncontrolled = true;
132 }
133 }
134
135 // TODO: Shouldn't this be getChecked(props)?
136 var checked = props.checked;
137 if (checked != null) {
138 DOMPropertyOperations.setValueForProperty(ReactDOMComponentTree.getNodeFromInstance(inst), 'checked', checked || false);
139 }
140
141 var node = ReactDOMComponentTree.getNodeFromInstance(inst);
142 var value = LinkedValueUtils.getValue(props);
143 if (value != null) {
144 if (value === 0 && node.value === '') {
145 node.value = '0';
146 // Note: IE9 reports a number inputs as 'text', so check props instead.
147 } else if (props.type === 'number') {
148 // Simulate `input.valueAsNumber`. IE9 does not support it
149 var valueAsNumber = parseFloat(node.value, 10) || 0;
150
151 if (
152 // eslint-disable-next-line
153 value != valueAsNumber ||
154 // eslint-disable-next-line
155 value == valueAsNumber && node.value != value) {
156 // Cast `value` to a string to ensure the value is set correctly. While
157 // browsers typically do this as necessary, jsdom doesn't.
158 node.value = '' + value;
159 }
160 } else if (node.value !== '' + value) {
161 // Cast `value` to a string to ensure the value is set correctly. While
162 // browsers typically do this as necessary, jsdom doesn't.
163 node.value = '' + value;
164 }
165 } else {
166 if (props.value == null && props.defaultValue != null) {
167 // In Chrome, assigning defaultValue to certain input types triggers input validation.
168 // For number inputs, the display value loses trailing decimal points. For email inputs,
169 // Chrome raises "The specified value <x> is not a valid email address".
170 //
171 // Here we check to see if the defaultValue has actually changed, avoiding these problems
172 // when the user is inputting text
173 //
174 // https://github.com/facebook/react/issues/7253
175 if (node.defaultValue !== '' + props.defaultValue) {
176 node.defaultValue = '' + props.defaultValue;
177 }
178 }
179 if (props.checked == null && props.defaultChecked != null) {
180 node.defaultChecked = !!props.defaultChecked;
181 }
182 }
183 },
184
185 postMountWrapper: function (inst) {
186 var props = inst._currentElement.props;
187
188 // This is in postMount because we need access to the DOM node, which is not
189 // available until after the component has mounted.
190 var node = ReactDOMComponentTree.getNodeFromInstance(inst);
191
192 // Detach value from defaultValue. We won't do anything if we're working on
193 // submit or reset inputs as those values & defaultValues are linked. They
194 // are not resetable nodes so this operation doesn't matter and actually
195 // removes browser-default values (eg "Submit Query") when no value is
196 // provided.
197
198 switch (props.type) {
199 case 'submit':
200 case 'reset':
201 break;
202 case 'color':
203 case 'date':
204 case 'datetime':
205 case 'datetime-local':
206 case 'month':
207 case 'time':
208 case 'week':
209 // This fixes the no-show issue on iOS Safari and Android Chrome:
210 // https://github.com/facebook/react/issues/7233
211 node.value = '';
212 node.value = node.defaultValue;
213 break;
214 default:
215 node.value = node.value;
216 break;
217 }
218
219 // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
220 // this is needed to work around a chrome bug where setting defaultChecked
221 // will sometimes influence the value of checked (even after detachment).
222 // Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
223 // We need to temporarily unset name to avoid disrupting radio button groups.
224 var name = node.name;
225 if (name !== '') {
226 node.name = '';
227 }
228 node.defaultChecked = !node.defaultChecked;
229 node.defaultChecked = !node.defaultChecked;
230 if (name !== '') {
231 node.name = name;
232 }
233 }
234};
235
236function _handleChange(event) {
237 var props = this._currentElement.props;
238
239 var returnValue = LinkedValueUtils.executeOnChange(props, event);
240
241 // Here we use asap to wait until all updates have propagated, which
242 // is important when using controlled components within layers:
243 // https://github.com/facebook/react/issues/1698
244 ReactUpdates.asap(forceUpdateIfMounted, this);
245
246 var name = props.name;
247 if (props.type === 'radio' && name != null) {
248 var rootNode = ReactDOMComponentTree.getNodeFromInstance(this);
249 var queryRoot = rootNode;
250
251 while (queryRoot.parentNode) {
252 queryRoot = queryRoot.parentNode;
253 }
254
255 // If `rootNode.form` was non-null, then we could try `form.elements`,
256 // but that sometimes behaves strangely in IE8. We could also try using
257 // `form.getElementsByName`, but that will only return direct children
258 // and won't include inputs that use the HTML5 `form=` attribute. Since
259 // the input might not even be in a form, let's just use the global
260 // `querySelectorAll` to ensure we don't miss anything.
261 var group = queryRoot.querySelectorAll('input[name=' + JSON.stringify('' + name) + '][type="radio"]');
262
263 for (var i = 0; i < group.length; i++) {
264 var otherNode = group[i];
265 if (otherNode === rootNode || otherNode.form !== rootNode.form) {
266 continue;
267 }
268 // This will throw if radio buttons rendered by different copies of React
269 // and the same name are rendered into the same form (same as #1939).
270 // That's probably okay; we don't support it just as we don't support
271 // mixing React radio buttons with non-React ones.
272 var otherInstance = ReactDOMComponentTree.getInstanceFromNode(otherNode);
273 !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;
274 // If this is a controlled radio button group, forcing the input that
275 // was previously checked to update will cause it to be come re-checked
276 // as appropriate.
277 ReactUpdates.asap(forceUpdateIfMounted, otherInstance);
278 }
279 }
280
281 return returnValue;
282}
283
284module.exports = ReactDOMInput;
\No newline at end of file