@samitouri / QOS-React / commits / 9c3e9fbb05

Fork ReactDOMComponent and the wrappers to Fiber

From now on any change to these have to be replicated to these files. I will next start changing the implementation to work with Fiber.

Sebastian Markbage committed Nov 15, 2016 at 15:54 UTC 9c3e9fbb057e3568f7f80bbefb79b9751e2078b2
5 files changed +2116
src/renderers/dom/fiber/ReactDOMFiberComponent.js new
+1303
@@ -0,0 +1,1303 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactDOMFiberComponent
10 + */
11 +
12 +/* global hasOwnProperty:true */
13 +
14 +'use strict';
15 +
16 +var AutoFocusUtils = require('AutoFocusUtils');
17 +var CSSPropertyOperations = require('CSSPropertyOperations');
18 +var DOMLazyTree = require('DOMLazyTree');
19 +var DOMNamespaces = require('DOMNamespaces');
20 +var DOMProperty = require('DOMProperty');
21 +var DOMPropertyOperations = require('DOMPropertyOperations');
22 +var EventPluginHub = require('EventPluginHub');
23 +var EventPluginRegistry = require('EventPluginRegistry');
24 +var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter');
25 +var ReactDOMComponentFlags = require('ReactDOMComponentFlags');
26 +var ReactDOMComponentTree = require('ReactDOMComponentTree');
27 +var ReactDOMInput = require('ReactDOMInput');
28 +var ReactDOMOption = require('ReactDOMOption');
29 +var ReactDOMSelect = require('ReactDOMSelect');
30 +var ReactDOMTextarea = require('ReactDOMTextarea');
31 +var ReactInstrumentation = require('ReactInstrumentation');
32 +var ReactMultiChild = require('ReactMultiChild');
33 +var ReactServerRenderingTransaction = require('ReactServerRenderingTransaction');
34 +
35 +var emptyFunction = require('emptyFunction');
36 +var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
37 +var invariant = require('invariant');
38 +var isEventSupported = require('isEventSupported');
39 +var shallowEqual = require('shallowEqual');
40 +var inputValueTracking = require('inputValueTracking');
41 +var validateDOMNesting = require('validateDOMNesting');
42 +var warning = require('warning');
43 +var didWarnShadyDOM = false;
44 +
45 +var Flags = ReactDOMComponentFlags;
46 +var deleteListener = EventPluginHub.deleteListener;
47 +var getNode = ReactDOMComponentTree.getNodeFromInstance;
48 +var listenTo = ReactBrowserEventEmitter.listenTo;
49 +var registrationNameModules = EventPluginRegistry.registrationNameModules;
50 +
51 +// For quickly matching children type, to test if can be treated as content.
52 +var CONTENT_TYPES = {'string': true, 'number': true};
53 +
54 +var STYLE = 'style';
55 +var HTML = '__html';
56 +var RESERVED_PROPS = {
57 + children: null,
58 + dangerouslySetInnerHTML: null,
59 + suppressContentEditableWarning: null,
60 +};
61 +
62 +// Node type for document fragments (Node.DOCUMENT_FRAGMENT_NODE).
63 +var DOC_FRAGMENT_TYPE = 11;
64 +
65 +
66 +function getDeclarationErrorAddendum(internalInstance) {
67 + if (internalInstance) {
68 + var owner = internalInstance._currentElement._owner || null;
69 + if (owner) {
70 + var name = owner.getName();
71 + if (name) {
72 + return ' This DOM node was rendered by `' + name + '`.';
73 + }
74 + }
75 + }
76 + return '';
77 +}
78 +
79 +function friendlyStringify(obj) {
80 + if (typeof obj === 'object') {
81 + if (Array.isArray(obj)) {
82 + return '[' + obj.map(friendlyStringify).join(', ') + ']';
83 + } else {
84 + var pairs = [];
85 + for (var key in obj) {
86 + if (Object.prototype.hasOwnProperty.call(obj, key)) {
87 + var keyEscaped = /^[a-z$_][\w$_]*$/i.test(key) ?
88 + key :
89 + JSON.stringify(key);
90 + pairs.push(keyEscaped + ': ' + friendlyStringify(obj[key]));
91 + }
92 + }
93 + return '{' + pairs.join(', ') + '}';
94 + }
95 + } else if (typeof obj === 'string') {
96 + return JSON.stringify(obj);
97 + } else if (typeof obj === 'function') {
98 + return '[function object]';
99 + }
100 + // Differs from JSON.stringify in that undefined because undefined and that
101 + // inf and nan don't become null
102 + return String(obj);
103 +}
104 +
105 +var styleMutationWarning = {};
106 +
107 +function checkAndWarnForMutatedStyle(style1, style2, component) {
108 + if (style1 == null || style2 == null) {
109 + return;
110 + }
111 + if (shallowEqual(style1, style2)) {
112 + return;
113 + }
114 +
115 + var componentName = component._tag;
116 + var owner = component._currentElement._owner;
117 + var ownerName;
118 + if (owner) {
119 + ownerName = owner.getName();
120 + }
121 +
122 + var hash = ownerName + '|' + componentName;
123 +
124 + if (styleMutationWarning.hasOwnProperty(hash)) {
125 + return;
126 + }
127 +
128 + styleMutationWarning[hash] = true;
129 +
130 + warning(
131 + false,
132 + '`%s` was passed a style object that has previously been mutated. ' +
133 + 'Mutating `style` is deprecated. Consider cloning it beforehand. Check ' +
134 + 'the `render` %s. Previous style: %s. Mutated style: %s.',
135 + componentName,
136 + owner ? 'of `' + ownerName + '`' : 'using <' + componentName + '>',
137 + friendlyStringify(style1),
138 + friendlyStringify(style2)
139 + );
140 +}
141 +
142 +/**
143 + * @param {object} component
144 + * @param {?object} props
145 + */
146 +function assertValidProps(component, props) {
147 + if (!props) {
148 + return;
149 + }
150 + // Note the use of `==` which checks for null or undefined.
151 + if (voidElementTags[component._tag]) {
152 + invariant(
153 + props.children == null && props.dangerouslySetInnerHTML == null,
154 + '%s is a void element tag and must neither have `children` nor ' +
155 + 'use `dangerouslySetInnerHTML`.%s',
156 + component._tag,
157 + component._currentElement._owner ?
158 + ' Check the render method of ' +
159 + component._currentElement._owner.getName() + '.' :
160 + ''
161 + );
162 + }
163 + if (props.dangerouslySetInnerHTML != null) {
164 + invariant(
165 + props.children == null,
166 + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
167 + );
168 + invariant(
169 + typeof props.dangerouslySetInnerHTML === 'object' &&
170 + HTML in props.dangerouslySetInnerHTML,
171 + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
172 + 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' +
173 + 'for more information.'
174 + );
175 + }
176 + if (__DEV__) {
177 + warning(
178 + props.innerHTML == null,
179 + 'Directly setting property `innerHTML` is not permitted. ' +
180 + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.'
181 + );
182 + warning(
183 + props.suppressContentEditableWarning ||
184 + !props.contentEditable ||
185 + props.children == null,
186 + 'A component is `contentEditable` and contains `children` managed by ' +
187 + 'React. It is now your responsibility to guarantee that none of ' +
188 + 'those nodes are unexpectedly modified or duplicated. This is ' +
189 + 'probably not intentional.'
190 + );
191 + warning(
192 + props.onFocusIn == null &&
193 + props.onFocusOut == null,
194 + 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
195 + 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
196 + 'are not needed/supported by React.'
197 + );
198 + }
199 + invariant(
200 + props.style == null || typeof props.style === 'object',
201 + 'The `style` prop expects a mapping from style properties to values, ' +
202 + 'not a string. For example, style={{marginRight: spacing + \'em\'}} when ' +
203 + 'using JSX.%s',
204 + getDeclarationErrorAddendum(component)
205 + );
206 +}
207 +
208 +function enqueuePutListener(inst, registrationName, listener, transaction) {
209 + if (transaction instanceof ReactServerRenderingTransaction) {
210 + return;
211 + }
212 + if (__DEV__) {
213 + // IE8 has no API for event capturing and the `onScroll` event doesn't
214 + // bubble.
215 + warning(
216 + registrationName !== 'onScroll' || isEventSupported('scroll', true),
217 + 'This browser doesn\'t support the `onScroll` event'
218 + );
219 + }
220 + var containerInfo = inst._hostContainerInfo;
221 + var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
222 + var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
223 + listenTo(registrationName, doc);
224 + transaction.getReactMountReady().enqueue(putListener, {
225 + inst: inst,
226 + registrationName: registrationName,
227 + listener: listener,
228 + });
229 +}
230 +
231 +// TODO: This is coming from future #8192. Dedupe this and enqueuePutListener.
232 +function ensureListeningTo(inst, registrationName, transaction) {
233 + if (transaction instanceof ReactServerRenderingTransaction) {
234 + return;
235 + }
236 + if (__DEV__) {
237 + // IE8 has no API for event capturing and the `onScroll` event doesn't
238 + // bubble.
239 + warning(
240 + registrationName !== 'onScroll' || isEventSupported('scroll', true),
241 + 'This browser doesn\'t support the `onScroll` event'
242 + );
243 + }
244 + var containerInfo = inst._hostContainerInfo;
245 + var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
246 + var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
247 + listenTo(registrationName, doc);
248 +}
249 +
250 +function putListener() {
251 + var listenerToPut = this;
252 + EventPluginHub.putListener(
253 + listenerToPut.inst,
254 + listenerToPut.registrationName,
255 + listenerToPut.listener
256 + );
257 +}
258 +
259 +function inputPostMount() {
260 + var inst = this;
261 + ReactDOMInput.postMountWrapper(inst);
262 +}
263 +
264 +function textareaPostMount() {
265 + var inst = this;
266 + ReactDOMTextarea.postMountWrapper(inst);
267 +}
268 +
269 +function optionPostMount() {
270 + var inst = this;
271 + ReactDOMOption.postMountWrapper(inst);
272 +}
273 +
274 +var setAndValidateContentChildDev = emptyFunction;
275 +if (__DEV__) {
276 + setAndValidateContentChildDev = function(content) {
277 + var hasExistingContent = this._contentDebugID != null;
278 + var debugID = this._debugID;
279 + // This ID represents the inlined child that has no backing instance:
280 + var contentDebugID = -debugID;
281 +
282 + if (content == null) {
283 + if (hasExistingContent) {
284 + ReactInstrumentation.debugTool.onUnmountComponent(this._contentDebugID);
285 + }
286 + this._contentDebugID = null;
287 + return;
288 + }
289 +
290 + validateDOMNesting(null, String(content), this, this._ancestorInfo);
291 + this._contentDebugID = contentDebugID;
292 + if (hasExistingContent) {
293 + ReactInstrumentation.debugTool.onBeforeUpdateComponent(contentDebugID, content);
294 + ReactInstrumentation.debugTool.onUpdateComponent(contentDebugID);
295 + } else {
296 + ReactInstrumentation.debugTool.onBeforeMountComponent(contentDebugID, content, debugID);
297 + ReactInstrumentation.debugTool.onMountComponent(contentDebugID);
298 + ReactInstrumentation.debugTool.onSetChildren(debugID, [contentDebugID]);
299 + }
300 + };
301 +}
302 +
303 +// There are so many media events, it makes sense to just
304 +// maintain a list rather than create a `trapBubbledEvent` for each
305 +var mediaEvents = {
306 + topAbort: 'abort',
307 + topCanPlay: 'canplay',
308 + topCanPlayThrough: 'canplaythrough',
309 + topDurationChange: 'durationchange',
310 + topEmptied: 'emptied',
311 + topEncrypted: 'encrypted',
312 + topEnded: 'ended',
313 + topError: 'error',
314 + topLoadedData: 'loadeddata',
315 + topLoadedMetadata: 'loadedmetadata',
316 + topLoadStart: 'loadstart',
317 + topPause: 'pause',
318 + topPlay: 'play',
319 + topPlaying: 'playing',
320 + topProgress: 'progress',
321 + topRateChange: 'ratechange',
322 + topSeeked: 'seeked',
323 + topSeeking: 'seeking',
324 + topStalled: 'stalled',
325 + topSuspend: 'suspend',
326 + topTimeUpdate: 'timeupdate',
327 + topVolumeChange: 'volumechange',
328 + topWaiting: 'waiting',
329 +};
330 +
331 +function trackInputValue() {
332 + inputValueTracking.track(this);
333 +}
334 +
335 +function trapClickOnNonInteractiveElement() {
336 + // Mobile Safari does not fire properly bubble click events on
337 + // non-interactive elements, which means delegated click listeners do not
338 + // fire. The workaround for this bug involves attaching an empty click
339 + // listener on the target node.
340 + // http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
341 + // Just set it using the onclick property so that we don't have to manage any
342 + // bookkeeping for it. Not sure if we need to clear it when the listener is
343 + // removed.
344 + // TODO: Only do this for the relevant Safaris maybe?
345 + var node = getNode(this);
346 + node.onclick = emptyFunction;
347 +}
348 +
349 +function trapBubbledEventsLocal() {
350 + var inst = this;
351 + // If a component renders to null or if another component fatals and causes
352 + // the state of the tree to be corrupted, `node` here can be null.
353 + invariant(inst._rootNodeID, 'Must be mounted to trap events');
354 + var node = getNode(inst);
355 + invariant(
356 + node,
357 + 'trapBubbledEvent(...): Requires node to be rendered.'
358 + );
359 +
360 + switch (inst._tag) {
361 + case 'iframe':
362 + case 'object':
363 + inst._wrapperState.listeners = [
364 + ReactBrowserEventEmitter.trapBubbledEvent(
365 + 'topLoad',
366 + 'load',
367 + node
368 + ),
369 + ];
370 + break;
371 + case 'video':
372 + case 'audio':
373 +
374 + inst._wrapperState.listeners = [];
375 + // Create listener for each media event
376 + for (var event in mediaEvents) {
377 + if (mediaEvents.hasOwnProperty(event)) {
378 + inst._wrapperState.listeners.push(
379 + ReactBrowserEventEmitter.trapBubbledEvent(
380 + event,
381 + mediaEvents[event],
382 + node
383 + )
384 + );
385 + }
386 + }
387 + break;
388 + case 'source':
389 + inst._wrapperState.listeners = [
390 + ReactBrowserEventEmitter.trapBubbledEvent(
391 + 'topError',
392 + 'error',
393 + node
394 + ),
395 + ];
396 + break;
397 + case 'img':
398 + inst._wrapperState.listeners = [
399 + ReactBrowserEventEmitter.trapBubbledEvent(
400 + 'topError',
401 + 'error',
402 + node
403 + ),
404 + ReactBrowserEventEmitter.trapBubbledEvent(
405 + 'topLoad',
406 + 'load',
407 + node
408 + ),
409 + ];
410 + break;
411 + case 'form':
412 + inst._wrapperState.listeners = [
413 + ReactBrowserEventEmitter.trapBubbledEvent(
414 + 'topReset',
415 + 'reset',
416 + node
417 + ),
418 + ReactBrowserEventEmitter.trapBubbledEvent(
419 + 'topSubmit',
420 + 'submit',
421 + node
422 + ),
423 + ];
424 + break;
425 + case 'input':
426 + case 'select':
427 + case 'textarea':
428 + inst._wrapperState.listeners = [
429 + ReactBrowserEventEmitter.trapBubbledEvent(
430 + 'topInvalid',
431 + 'invalid',
432 + node
433 + ),
434 + ];
435 + break;
436 + }
437 +}
438 +
439 +function postUpdateSelectWrapper() {
440 + ReactDOMSelect.postUpdateWrapper(this);
441 +}
442 +
443 +// For HTML, certain tags should omit their close tag. We keep a whitelist for
444 +// those special-case tags.
445 +
446 +var omittedCloseTags = {
447 + 'area': true,
448 + 'base': true,
449 + 'br': true,
450 + 'col': true,
451 + 'embed': true,
452 + 'hr': true,
453 + 'img': true,
454 + 'input': true,
455 + 'keygen': true,
456 + 'link': true,
457 + 'meta': true,
458 + 'param': true,
459 + 'source': true,
460 + 'track': true,
461 + 'wbr': true,
462 + // NOTE: menuitem's close tag should be omitted, but that causes problems.
463 +};
464 +
465 +var newlineEatingTags = {
466 + 'listing': true,
467 + 'pre': true,
468 + 'textarea': true,
469 +};
470 +
471 +// For HTML, certain tags cannot have children. This has the same purpose as
472 +// `omittedCloseTags` except that `menuitem` should still have its closing tag.
473 +
474 +var voidElementTags = Object.assign({
475 + 'menuitem': true,
476 +}, omittedCloseTags);
477 +
478 +// We accept any tag to be rendered but since this gets injected into arbitrary
479 +// HTML, we want to make sure that it's a safe tag.
480 +// http://www.w3.org/TR/REC-xml/#NT-Name
481 +
482 +var VALID_TAG_REGEX = /^[a-zA-Z][a-zA-Z:_\.\-\d]*$/; // Simplified subset
483 +var validatedTagCache = {};
484 +var hasOwnProperty = {}.hasOwnProperty;
485 +
486 +function validateDangerousTag(tag) {
487 + if (!hasOwnProperty.call(validatedTagCache, tag)) {
488 + invariant(VALID_TAG_REGEX.test(tag), 'Invalid tag: %s', tag);
489 + validatedTagCache[tag] = true;
490 + }
491 +}
492 +
493 +function isCustomComponent(tagName, props) {
494 + return tagName.indexOf('-') >= 0 || props.is != null;
495 +}
496 +
497 +var globalIdCounter = 1;
498 +
499 +/**
500 + * Creates a new React class that is idempotent and capable of containing other
501 + * React components. It accepts event listeners and DOM properties that are
502 + * valid according to `DOMProperty`.
503 + *
504 + * - Event listeners: `onClick`, `onMouseDown`, etc.
505 + * - DOM properties: `className`, `name`, `title`, etc.
506 + *
507 + * The `style` property functions differently from the DOM API. It accepts an
508 + * object mapping of style properties to values.
509 + *
510 + * @constructor ReactDOMComponent
511 + * @extends ReactMultiChild
512 + */
513 +function ReactDOMComponent(element) {
514 + var tag = element.type;
515 + validateDangerousTag(tag);
516 + this._currentElement = element;
517 + this._tag = tag.toLowerCase();
518 + this._namespaceURI = null;
519 + this._renderedChildren = null;
520 + this._previousStyle = null;
521 + this._previousStyleCopy = null;
522 + this._hostNode = null;
523 + this._hostParent = null;
524 + this._rootNodeID = 0;
525 + this._domID = 0;
526 + this._hostContainerInfo = null;
527 + this._wrapperState = null;
528 + this._topLevelWrapper = null;
529 + this._flags = 0;
530 + if (__DEV__) {
531 + this._ancestorInfo = null;
532 + setAndValidateContentChildDev.call(this, null);
533 + }
534 +}
535 +
536 +ReactDOMComponent.displayName = 'ReactDOMComponent';
537 +
538 +ReactDOMComponent.Mixin = {
539 +
540 + /**
541 + * Generates root tag markup then recurses. This method has side effects and
542 + * is not idempotent.
543 + *
544 + * @internal
545 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
546 + * @param {?ReactDOMComponent} the parent component instance
547 + * @param {?object} info about the host container
548 + * @param {object} context
549 + * @return {string} The computed markup.
550 + */
551 + mountComponent: function(
552 + transaction,
553 + hostParent,
554 + hostContainerInfo,
555 + context
556 + ) {
557 + this._rootNodeID = globalIdCounter++;
558 + this._domID = hostContainerInfo._idCounter++;
559 + this._hostParent = hostParent;
560 + this._hostContainerInfo = hostContainerInfo;
561 +
562 + var props = this._currentElement.props;
563 +
564 + switch (this._tag) {
565 + case 'audio':
566 + case 'form':
567 + case 'iframe':
568 + case 'img':
569 + case 'link':
570 + case 'object':
571 + case 'source':
572 + case 'video':
573 + this._wrapperState = {
574 + listeners: null,
575 + };
576 + transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
577 + break;
578 + case 'input':
579 + ReactDOMInput.mountWrapper(this, props, hostParent);
580 + props = ReactDOMInput.getHostProps(this, props);
581 + transaction.getReactMountReady().enqueue(trackInputValue, this);
582 + transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
583 + // For controlled components we always need to ensure we're listening
584 + // to onChange. Even if there is no listener.
585 + ensureListeningTo(this, 'onChange', transaction);
586 + break;
587 + case 'option':
588 + ReactDOMOption.mountWrapper(this, props, hostParent);
589 + props = ReactDOMOption.getHostProps(this, props);
590 + break;
591 + case 'select':
592 + ReactDOMSelect.mountWrapper(this, props, hostParent);
593 + props = ReactDOMSelect.getHostProps(this, props);
594 + transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
595 + // For controlled components we always need to ensure we're listening
596 + // to onChange. Even if there is no listener.
597 + ensureListeningTo(this, 'onChange', transaction);
598 + break;
599 + case 'textarea':
600 + ReactDOMTextarea.mountWrapper(this, props, hostParent);
601 + props = ReactDOMTextarea.getHostProps(this, props);
602 + transaction.getReactMountReady().enqueue(trackInputValue, this);
603 + transaction.getReactMountReady().enqueue(trapBubbledEventsLocal, this);
604 + // For controlled components we always need to ensure we're listening
605 + // to onChange. Even if there is no listener.
606 + ensureListeningTo(this, 'onChange', transaction);
607 + break;
608 + }
609 +
610 + assertValidProps(this, props);
611 +
612 + // We create tags in the namespace of their parent container, except HTML
613 + // tags get no namespace.
614 + var namespaceURI;
615 + var parentTag;
616 + if (hostParent != null) {
617 + namespaceURI = hostParent._namespaceURI;
618 + parentTag = hostParent._tag;
619 + } else if (hostContainerInfo._tag) {
620 + namespaceURI = hostContainerInfo._namespaceURI;
621 + parentTag = hostContainerInfo._tag;
622 + }
623 + if (namespaceURI == null ||
624 + namespaceURI === DOMNamespaces.svg && parentTag === 'foreignobject') {
625 + namespaceURI = DOMNamespaces.html;
626 + }
627 + if (namespaceURI === DOMNamespaces.html) {
628 + if (this._tag === 'svg') {
629 + namespaceURI = DOMNamespaces.svg;
630 + } else if (this._tag === 'math') {
631 + namespaceURI = DOMNamespaces.mathml;
632 + }
633 + }
634 + this._namespaceURI = namespaceURI;
635 +
636 + if (__DEV__) {
637 + var parentInfo;
638 + if (hostParent != null) {
639 + parentInfo = hostParent._ancestorInfo;
640 + } else if (hostContainerInfo._tag) {
641 + parentInfo = hostContainerInfo._ancestorInfo;
642 + }
643 + if (parentInfo) {
644 + // parentInfo should always be present except for the top-level
645 + // component when server rendering
646 + validateDOMNesting(this._tag, null, this, parentInfo);
647 + }
648 + this._ancestorInfo =
649 + validateDOMNesting.updatedAncestorInfo(parentInfo, this._tag, this);
650 + }
651 +
652 + var mountImage;
653 + var type = this._currentElement.type;
654 + if (transaction.useCreateElement) {
655 + var ownerDocument = hostContainerInfo._ownerDocument;
656 + var el;
657 + if (namespaceURI === DOMNamespaces.html) {
658 + if (this._tag === 'script') {
659 + // Create the script via .innerHTML so its "parser-inserted" flag is
660 + // set to true and it does not execute
661 + var div = ownerDocument.createElement('div');
662 + div.innerHTML = `<${type}></${type}>`;
663 + el = div.removeChild(div.firstChild);
664 + } else if (props.is) {
665 + el = ownerDocument.createElement(type, props.is);
666 + } else {
667 + // Separate else branch instead of using `props.is || undefined` above becuase of a Firefox bug.
668 + // See discussion in https://github.com/facebook/react/pull/6896
669 + // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
670 + el = ownerDocument.createElement(type);
671 + }
672 + } else {
673 + el = ownerDocument.createElementNS(
674 + namespaceURI,
675 + type
676 + );
677 + }
678 + var isCustomComponentTag = isCustomComponent(this._tag, props);
679 + if (__DEV__ && isCustomComponentTag && !didWarnShadyDOM && el.shadyRoot) {
680 + var owner = this._currentElement._owner;
681 + var name = owner && owner.getName() || 'A component';
682 + warning(
683 + false,
684 + '%s is using shady DOM. Using shady DOM with React can ' +
685 + 'cause things to break subtly.',
686 + name
687 + );
688 + didWarnShadyDOM = true;
689 + }
690 + ReactDOMComponentTree.precacheNode(this, el);
691 + this._flags |= Flags.hasCachedChildNodes;
692 + if (!this._hostParent) {
693 + DOMPropertyOperations.setAttributeForRoot(el);
694 + }
695 + this._updateDOMProperties(null, props, transaction, isCustomComponentTag);
696 + var lazyTree = DOMLazyTree(el);
697 + this._createInitialChildren(transaction, props, context, lazyTree);
698 + mountImage = lazyTree;
699 + } else {
700 + var tagOpen = this._createOpenTagMarkupAndPutListeners(transaction, props);
701 + var tagContent = this._createContentMarkup(transaction, props, context);
702 + if (!tagContent && omittedCloseTags[this._tag]) {
703 + mountImage = tagOpen + '/>';
704 + } else {
705 + mountImage = tagOpen + '>' + tagContent + '</' + type + '>';
706 + }
707 + }
708 +
709 + switch (this._tag) {
710 + case 'input':
711 + transaction.getReactMountReady().enqueue(
712 + inputPostMount,
713 + this
714 + );
715 + if (props.autoFocus) {
716 + transaction.getReactMountReady().enqueue(
717 + AutoFocusUtils.focusDOMComponent,
718 + this
719 + );
720 + }
721 + break;
722 + case 'textarea':
723 + transaction.getReactMountReady().enqueue(
724 + textareaPostMount,
725 + this
726 + );
727 + if (props.autoFocus) {
728 + transaction.getReactMountReady().enqueue(
729 + AutoFocusUtils.focusDOMComponent,
730 + this
731 + );
732 + }
733 + break;
734 + case 'select':
735 + if (props.autoFocus) {
736 + transaction.getReactMountReady().enqueue(
737 + AutoFocusUtils.focusDOMComponent,
738 + this
739 + );
740 + }
741 + break;
742 + case 'button':
743 + if (props.autoFocus) {
744 + transaction.getReactMountReady().enqueue(
745 + AutoFocusUtils.focusDOMComponent,
746 + this
747 + );
748 + }
749 + break;
750 + case 'option':
751 + transaction.getReactMountReady().enqueue(
752 + optionPostMount,
753 + this
754 + );
755 + break;
756 + default:
757 + if (typeof props.onClick === 'function') {
758 + transaction.getReactMountReady().enqueue(
759 + trapClickOnNonInteractiveElement,
760 + this
761 + );
762 + }
763 + break;
764 + }
765 +
766 + return mountImage;
767 + },
768 +
769 + /**
770 + * Creates markup for the open tag and all attributes.
771 + *
772 + * This method has side effects because events get registered.
773 + *
774 + * Iterating over object properties is faster than iterating over arrays.
775 + * @see http://jsperf.com/obj-vs-arr-iteration
776 + *
777 + * @private
778 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
779 + * @param {object} props
780 + * @return {string} Markup of opening tag.
781 + */
782 + _createOpenTagMarkupAndPutListeners: function(transaction, props) {
783 + var ret = '<' + this._currentElement.type;
784 +
785 + for (var propKey in props) {
786 + if (!props.hasOwnProperty(propKey)) {
787 + continue;
788 + }
789 + var propValue = props[propKey];
790 + if (propValue == null) {
791 + continue;
792 + }
793 + if (registrationNameModules.hasOwnProperty(propKey)) {
794 + if (propValue) {
795 + enqueuePutListener(this, propKey, propValue, transaction);
796 + }
797 + } else {
798 + if (propKey === STYLE) {
799 + if (propValue) {
800 + if (__DEV__) {
801 + // See `_updateDOMProperties`. style block
802 + this._previousStyle = propValue;
803 + }
804 + propValue = this._previousStyleCopy = Object.assign({}, props.style);
805 + }
806 + propValue = CSSPropertyOperations.createMarkupForStyles(propValue, this);
807 + }
808 + var markup = null;
809 + if (this._tag != null && isCustomComponent(this._tag, props)) {
810 + if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
811 + markup = DOMPropertyOperations.createMarkupForCustomAttribute(propKey, propValue);
812 + }
813 + } else {
814 + markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue);
815 + }
816 + if (markup) {
817 + ret += ' ' + markup;
818 + }
819 + }
820 + }
821 +
822 + // For static pages, no need to put React ID and checksum. Saves lots of
823 + // bytes.
824 + if (transaction.renderToStaticMarkup) {
825 + return ret;
826 + }
827 +
828 + if (!this._hostParent) {
829 + ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
830 + }
831 + ret += ' ' + DOMPropertyOperations.createMarkupForID(this._domID);
832 + return ret;
833 + },
834 +
835 + /**
836 + * Creates markup for the content between the tags.
837 + *
838 + * @private
839 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
840 + * @param {object} props
841 + * @param {object} context
842 + * @return {string} Content markup.
843 + */
844 + _createContentMarkup: function(transaction, props, context) {
845 + var ret = '';
846 +
847 + // Intentional use of != to avoid catching zero/false.
848 + var innerHTML = props.dangerouslySetInnerHTML;
849 + if (innerHTML != null) {
850 + if (innerHTML.__html != null) {
851 + ret = innerHTML.__html;
852 + }
853 + } else {
854 + var contentToUse =
855 + CONTENT_TYPES[typeof props.children] ? props.children : null;
856 + var childrenToUse = contentToUse != null ? null : props.children;
857 + if (contentToUse != null) {
858 + // TODO: Validate that text is allowed as a child of this node
859 + ret = escapeTextContentForBrowser(contentToUse);
860 + if (__DEV__) {
861 + setAndValidateContentChildDev.call(this, contentToUse);
862 + }
863 + } else if (childrenToUse != null) {
864 + var mountImages = this.mountChildren(
865 + childrenToUse,
866 + transaction,
867 + context
868 + );
869 + ret = mountImages.join('');
870 + }
871 + }
872 + if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
873 + // text/html ignores the first character in these tags if it's a newline
874 + // Prefer to break application/xml over text/html (for now) by adding
875 + // a newline specifically to get eaten by the parser. (Alternately for
876 + // textareas, replacing "^\n" with "\r\n" doesn't get eaten, and the first
877 + // \r is normalized out by HTMLTextAreaElement#value.)
878 + // See: <http://www.w3.org/TR/html-polyglot/#newlines-in-textarea-and-pre>
879 + // See: <http://www.w3.org/TR/html5/syntax.html#element-restrictions>
880 + // See: <http://www.w3.org/TR/html5/syntax.html#newlines>
881 + // See: Parsing of "textarea" "listing" and "pre" elements
882 + // from <http://www.w3.org/TR/html5/syntax.html#parsing-main-inbody>
883 + return '\n' + ret;
884 + } else {
885 + return ret;
886 + }
887 + },
888 +
889 + _createInitialChildren: function(transaction, props, context, lazyTree) {
890 + // Intentional use of != to avoid catching zero/false.
891 + var innerHTML = props.dangerouslySetInnerHTML;
892 + if (innerHTML != null) {
893 + if (innerHTML.__html != null) {
894 + DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
895 + }
896 + } else {
897 + var contentToUse =
898 + CONTENT_TYPES[typeof props.children] ? props.children : null;
899 + var childrenToUse = contentToUse != null ? null : props.children;
900 + // TODO: Validate that text is allowed as a child of this node
901 + if (contentToUse != null) {
902 + // Avoid setting textContent when the text is empty. In IE11 setting
903 + // textContent on a text area will cause the placeholder to not
904 + // show within the textarea until it has been focused and blurred again.
905 + // https://github.com/facebook/react/issues/6731#issuecomment-254874553
906 + if (contentToUse !== '') {
907 + if (__DEV__) {
908 + setAndValidateContentChildDev.call(this, contentToUse);
909 + }
910 + DOMLazyTree.queueText(lazyTree, contentToUse);
911 + }
912 + } else if (childrenToUse != null) {
913 + var mountImages = this.mountChildren(
914 + childrenToUse,
915 + transaction,
916 + context
917 + );
918 + for (var i = 0; i < mountImages.length; i++) {
919 + DOMLazyTree.queueChild(lazyTree, mountImages[i]);
920 + }
921 + }
922 + }
923 + },
924 +
925 + /**
926 + * Receives a next element and updates the component.
927 + *
928 + * @internal
929 + * @param {ReactElement} nextElement
930 + * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
931 + * @param {object} context
932 + */
933 + receiveComponent: function(nextElement, transaction, context) {
934 + var prevElement = this._currentElement;
935 + this._currentElement = nextElement;
936 + this.updateComponent(transaction, prevElement, nextElement, context);
937 + },
938 +
939 + /**
940 + * Updates a DOM component after it has already been allocated and
941 + * attached to the DOM. Reconciles the root DOM node, then recurses.
942 + *
943 + * @param {ReactReconcileTransaction} transaction
944 + * @param {ReactElement} prevElement
945 + * @param {ReactElement} nextElement
946 + * @internal
947 + * @overridable
948 + */
949 + updateComponent: function(transaction, prevElement, nextElement, context) {
950 + var lastProps = prevElement.props;
951 + var nextProps = this._currentElement.props;
952 +
953 + switch (this._tag) {
954 + case 'input':
955 + lastProps = ReactDOMInput.getHostProps(this, lastProps);
956 + nextProps = ReactDOMInput.getHostProps(this, nextProps);
957 + break;
958 + case 'option':
959 + lastProps = ReactDOMOption.getHostProps(this, lastProps);
960 + nextProps = ReactDOMOption.getHostProps(this, nextProps);
961 + break;
962 + case 'select':
963 + lastProps = ReactDOMSelect.getHostProps(this, lastProps);
964 + nextProps = ReactDOMSelect.getHostProps(this, nextProps);
965 + break;
966 + case 'textarea':
967 + lastProps = ReactDOMTextarea.getHostProps(this, lastProps);
968 + nextProps = ReactDOMTextarea.getHostProps(this, nextProps);
969 + break;
970 + default:
971 + if (typeof lastProps.onClick !== 'function' &&
972 + typeof nextProps.onClick === 'function') {
973 + transaction.getReactMountReady().enqueue(
974 + trapClickOnNonInteractiveElement,
975 + this
976 + );
977 + }
978 + break;
979 + }
980 +
981 + assertValidProps(this, nextProps);
982 + var isCustomComponentTag = isCustomComponent(this._tag, nextProps);
983 + this._updateDOMProperties(lastProps, nextProps, transaction, isCustomComponentTag);
984 + this._updateDOMChildren(
985 + lastProps,
986 + nextProps,
987 + transaction,
988 + context
989 + );
990 +
991 + switch (this._tag) {
992 + case 'input':
993 + // Update the wrapper around inputs *after* updating props. This has to
994 + // happen after `_updateDOMProperties`. Otherwise HTML5 input validations
995 + // raise warnings and prevent the new value from being assigned.
996 + ReactDOMInput.updateWrapper(this);
997 + break;
998 + case 'textarea':
999 + ReactDOMTextarea.updateWrapper(this);
1000 + break;
1001 + case 'select':
1002 + // <select> value update needs to occur after <option> children
1003 + // reconciliation
1004 + transaction.getReactMountReady().enqueue(postUpdateSelectWrapper, this);
1005 + break;
1006 + }
1007 + },
1008 +
1009 + /**
1010 + * Reconciles the properties by detecting differences in property values and
1011 + * updating the DOM as necessary. This function is probably the single most
1012 + * critical path for performance optimization.
1013 + *
1014 + * TODO: Benchmark whether checking for changed values in memory actually
1015 + * improves performance (especially statically positioned elements).
1016 + * TODO: Benchmark the effects of putting this at the top since 99% of props
1017 + * do not change for a given reconciliation.
1018 + * TODO: Benchmark areas that can be improved with caching.
1019 + *
1020 + * @private
1021 + * @param {object} lastProps
1022 + * @param {object} nextProps
1023 + * @param {?DOMElement} node
1024 + */
1025 + _updateDOMProperties: function(
1026 + lastProps,
1027 + nextProps,
1028 + transaction,
1029 + isCustomComponentTag
1030 + ) {
1031 + var propKey;
1032 + var styleName;
1033 + var styleUpdates;
1034 + for (propKey in lastProps) {
1035 + if (nextProps.hasOwnProperty(propKey) ||
1036 + !lastProps.hasOwnProperty(propKey) ||
1037 + lastProps[propKey] == null) {
1038 + continue;
1039 + }
1040 + if (propKey === STYLE) {
1041 + var lastStyle = this._previousStyleCopy;
1042 + for (styleName in lastStyle) {
1043 + if (lastStyle.hasOwnProperty(styleName)) {
1044 + styleUpdates = styleUpdates || {};
1045 + styleUpdates[styleName] = '';
1046 + }
1047 + }
1048 + this._previousStyleCopy = null;
1049 + } else if (registrationNameModules.hasOwnProperty(propKey)) {
1050 + if (lastProps[propKey]) {
1051 + // Only call deleteListener if there was a listener previously or
1052 + // else willDeleteListener gets called when there wasn't actually a
1053 + // listener (e.g., onClick={null})
1054 + deleteListener(this, propKey);
1055 + }
1056 + } else if (isCustomComponent(this._tag, lastProps)) {
1057 + if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
1058 + DOMPropertyOperations.deleteValueForAttribute(
1059 + getNode(this),
1060 + propKey
1061 + );
1062 + }
1063 + } else if (
1064 + DOMProperty.properties[propKey] ||
1065 + DOMProperty.isCustomAttribute(propKey)) {
1066 + DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey);
1067 + }
1068 + }
1069 + for (propKey in nextProps) {
1070 + var nextProp = nextProps[propKey];
1071 + var lastProp =
1072 + propKey === STYLE ? this._previousStyleCopy :
1073 + lastProps != null ? lastProps[propKey] : undefined;
1074 + if (!nextProps.hasOwnProperty(propKey) ||
1075 + nextProp === lastProp ||
1076 + nextProp == null && lastProp == null) {
1077 + continue;
1078 + }
1079 + if (propKey === STYLE) {
1080 + if (nextProp) {
1081 + if (__DEV__) {
1082 + checkAndWarnForMutatedStyle(
1083 + this._previousStyleCopy,
1084 + this._previousStyle,
1085 + this
1086 + );
1087 + this._previousStyle = nextProp;
1088 + }
1089 + nextProp = this._previousStyleCopy = Object.assign({}, nextProp);
1090 + } else {
1091 + this._previousStyleCopy = null;
1092 + }
1093 + if (lastProp) {
1094 + // Unset styles on `lastProp` but not on `nextProp`.
1095 + for (styleName in lastProp) {
1096 + if (lastProp.hasOwnProperty(styleName) &&
1097 + (!nextProp || !nextProp.hasOwnProperty(styleName))) {
1098 + styleUpdates = styleUpdates || {};
1099 + styleUpdates[styleName] = '';
1100 + }
1101 + }
1102 + // Update styles that changed since `lastProp`.
1103 + for (styleName in nextProp) {
1104 + if (nextProp.hasOwnProperty(styleName) &&
1105 + lastProp[styleName] !== nextProp[styleName]) {
1106 + styleUpdates = styleUpdates || {};
1107 + styleUpdates[styleName] = nextProp[styleName];
1108 + }
1109 + }
1110 + } else {
1111 + // Relies on `updateStylesByID` not mutating `styleUpdates`.
1112 + styleUpdates = nextProp;
1113 + }
1114 + } else if (registrationNameModules.hasOwnProperty(propKey)) {
1115 + if (nextProp) {
1116 + enqueuePutListener(this, propKey, nextProp, transaction);
1117 + } else if (lastProp) {
1118 + deleteListener(this, propKey);
1119 + }
1120 + } else if (isCustomComponentTag) {
1121 + if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
1122 + DOMPropertyOperations.setValueForAttribute(
1123 + getNode(this),
1124 + propKey,
1125 + nextProp
1126 + );
1127 + }
1128 + } else if (
1129 + DOMProperty.properties[propKey] ||
1130 + DOMProperty.isCustomAttribute(propKey)) {
1131 + var node = getNode(this);
1132 + // If we're updating to null or undefined, we should remove the property
1133 + // from the DOM node instead of inadvertently setting to a string. This
1134 + // brings us in line with the same behavior we have on initial render.
1135 + if (nextProp != null) {
1136 + DOMPropertyOperations.setValueForProperty(node, propKey, nextProp);
1137 + } else {
1138 + DOMPropertyOperations.deleteValueForProperty(node, propKey);
1139 + }
1140 + }
1141 + }
1142 + if (styleUpdates) {
1143 + CSSPropertyOperations.setValueForStyles(
1144 + getNode(this),
1145 + styleUpdates,
1146 + this
1147 + );
1148 + }
1149 + },
1150 +
1151 + /**
1152 + * Reconciles the children with the various properties that affect the
1153 + * children content.
1154 + *
1155 + * @param {object} lastProps
1156 + * @param {object} nextProps
1157 + * @param {ReactReconcileTransaction} transaction
1158 + * @param {object} context
1159 + */
1160 + _updateDOMChildren: function(lastProps, nextProps, transaction, context) {
1161 + var lastContent =
1162 + CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
1163 + var nextContent =
1164 + CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
1165 +
1166 + var lastHtml =
1167 + lastProps.dangerouslySetInnerHTML &&
1168 + lastProps.dangerouslySetInnerHTML.__html;
1169 + var nextHtml =
1170 + nextProps.dangerouslySetInnerHTML &&
1171 + nextProps.dangerouslySetInnerHTML.__html;
1172 +
1173 + // Note the use of `!=` which checks for null or undefined.
1174 + var lastChildren = lastContent != null ? null : lastProps.children;
1175 + var nextChildren = nextContent != null ? null : nextProps.children;
1176 +
1177 + // If we're switching from children to content/html or vice versa, remove
1178 + // the old content
1179 + var lastHasContentOrHtml = lastContent != null || lastHtml != null;
1180 + var nextHasContentOrHtml = nextContent != null || nextHtml != null;
1181 + if (lastChildren != null && nextChildren == null) {
1182 + this.updateChildren(null, transaction, context);
1183 + } else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
1184 + this.updateTextContent('');
1185 + if (__DEV__) {
1186 + ReactInstrumentation.debugTool.onSetChildren(this._debugID, []);
1187 + }
1188 + }
1189 +
1190 + if (nextContent != null) {
1191 + if (lastContent !== nextContent) {
1192 + this.updateTextContent('' + nextContent);
1193 + if (__DEV__) {
1194 + setAndValidateContentChildDev.call(this, nextContent);
1195 + }
1196 + }
1197 + } else if (nextHtml != null) {
1198 + if (lastHtml !== nextHtml) {
1199 + this.updateMarkup('' + nextHtml);
1200 + }
1201 + if (__DEV__) {
1202 + ReactInstrumentation.debugTool.onSetChildren(this._debugID, []);
1203 + }
1204 + } else if (nextChildren != null) {
1205 + if (__DEV__) {
1206 + setAndValidateContentChildDev.call(this, null);
1207 + }
1208 +
1209 + this.updateChildren(nextChildren, transaction, context);
1210 + }
1211 + },
1212 +
1213 + getHostNode: function() {
1214 + return getNode(this);
1215 + },
1216 +
1217 + /**
1218 + * Destroys all event registrations for this instance. Does not remove from
1219 + * the DOM. That must be done by the parent.
1220 + *
1221 + * @internal
1222 + */
1223 + unmountComponent: function(safely, skipLifecycle) {
1224 + switch (this._tag) {
1225 + case 'audio':
1226 + case 'form':
1227 + case 'iframe':
1228 + case 'img':
1229 + case 'link':
1230 + case 'object':
1231 + case 'source':
1232 + case 'video':
1233 + var listeners = this._wrapperState.listeners;
1234 + if (listeners) {
1235 + for (var i = 0; i < listeners.length; i++) {
1236 + listeners[i].remove();
1237 + }
1238 + }
1239 + break;
1240 + case 'input':
1241 + case 'textarea':
1242 + inputValueTracking.stopTracking(this);
1243 + break;
1244 + case 'html':
1245 + case 'head':
1246 + case 'body':
1247 + /**
1248 + * Components like <html> <head> and <body> can't be removed or added
1249 + * easily in a cross-browser way, however it's valuable to be able to
1250 + * take advantage of React's reconciliation for styling and <title>
1251 + * management. So we just document it and throw in dangerous cases.
1252 + */
1253 + invariant(
1254 + false,
1255 + '<%s> tried to unmount. Because of cross-browser quirks it is ' +
1256 + 'impossible to unmount some top-level components (eg <html>, ' +
1257 + '<head>, and <body>) reliably and efficiently. To fix this, have a ' +
1258 + 'single top-level component that never unmounts render these ' +
1259 + 'elements.',
1260 + this._tag
1261 + );
1262 + break;
1263 + }
1264 +
1265 + this.unmountChildren(safely, skipLifecycle);
1266 + ReactDOMComponentTree.uncacheNode(this);
1267 + EventPluginHub.deleteAllListeners(this);
1268 + this._rootNodeID = 0;
1269 + this._domID = 0;
1270 + this._wrapperState = null;
1271 +
1272 + if (__DEV__) {
1273 + setAndValidateContentChildDev.call(this, null);
1274 + }
1275 + },
1276 +
1277 + restoreControlledState: function() {
1278 + switch (this._tag) {
1279 + case 'input':
1280 + ReactDOMInput.restoreControlledState(this);
1281 + return;
1282 + case 'textarea':
1283 + ReactDOMTextarea.restoreControlledState(this);
1284 + return;
1285 + case 'select':
1286 + ReactDOMSelect.restoreControlledState(this);
1287 + return;
1288 + }
1289 + },
1290 +
1291 + getPublicInstance: function() {
1292 + return getNode(this);
1293 + },
1294 +
1295 +};
1296 +
1297 +Object.assign(
1298 + ReactDOMComponent.prototype,
1299 + ReactDOMComponent.Mixin,
1300 + ReactMultiChild
1301 +);
1302 +
1303 +module.exports = ReactDOMComponent;
src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js new
+313
@@ -0,0 +1,313 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactDOMFiberInput
10 + */
11 +
12 +'use strict';
13 +
14 +var DOMPropertyOperations = require('DOMPropertyOperations');
15 +var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes');
16 +var ReactDOMComponentTree = require('ReactDOMComponentTree');
17 +
18 +var invariant = require('invariant');
19 +var warning = require('warning');
20 +
21 +var didWarnValueDefaultValue = false;
22 +var didWarnCheckedDefaultChecked = false;
23 +var didWarnControlledToUncontrolled = false;
24 +var didWarnUncontrolledToControlled = false;
25 +
26 +function isControlled(props) {
27 + var usesChecked = props.type === 'checkbox' || props.type === 'radio';
28 + return usesChecked ? props.checked != null : props.value != null;
29 +}
30 +
31 +/**
32 + * Implements an <input> host component that allows setting these optional
33 + * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
34 + *
35 + * If `checked` or `value` are not supplied (or null/undefined), user actions
36 + * that affect the checked state or value will trigger updates to the element.
37 + *
38 + * If they are supplied (and not null/undefined), the rendered element will not
39 + * trigger updates to the element. Instead, the props must change in order for
40 + * the rendered element to be updated.
41 + *
42 + * The rendered element will be initialized as unchecked (or `defaultChecked`)
43 + * with an empty value (or `defaultValue`).
44 + *
45 + * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
46 + */
47 +var ReactDOMInput = {
48 + getHostProps: function(inst, props) {
49 + var value = props.value;
50 + var checked = props.checked;
51 +
52 + var hostProps = Object.assign({
53 + // Make sure we set .type before any other properties (setting .value
54 + // before .type means .value is lost in IE11 and below)
55 + type: undefined,
56 + // Make sure we set .step before .value (setting .value before .step
57 + // means .value is rounded on mount, based upon step precision)
58 + step: undefined,
59 + // Make sure we set .min & .max before .value (to ensure proper order
60 + // in corner cases such as min or max deriving from value, e.g. Issue #7170)
61 + min: undefined,
62 + max: undefined,
63 + }, props, {
64 + defaultChecked: undefined,
65 + defaultValue: undefined,
66 + value: value != null ? value : inst._wrapperState.initialValue,
67 + checked: checked != null ? checked : inst._wrapperState.initialChecked,
68 + });
69 +
70 + return hostProps;
71 + },
72 +
73 + mountWrapper: function(inst, props) {
74 + if (__DEV__) {
75 + ReactControlledValuePropTypes.checkPropTypes(
76 + 'input',
77 + props,
78 + inst._currentElement._owner
79 + );
80 +
81 + var owner = inst._currentElement._owner;
82 +
83 + if (
84 + props.checked !== undefined &&
85 + props.defaultChecked !== undefined &&
86 + !didWarnCheckedDefaultChecked
87 + ) {
88 + warning(
89 + false,
90 + '%s contains an input of type %s with both checked and defaultChecked props. ' +
91 + 'Input elements must be either controlled or uncontrolled ' +
92 + '(specify either the checked prop, or the defaultChecked prop, but not ' +
93 + 'both). Decide between using a controlled or uncontrolled input ' +
94 + 'element and remove one of these props. More info: ' +
95 + 'https://fb.me/react-controlled-components',
96 + owner && owner.getName() || 'A component',
97 + props.type
98 + );
99 + didWarnCheckedDefaultChecked = true;
100 + }
101 + if (
102 + props.value !== undefined &&
103 + props.defaultValue !== undefined &&
104 + !didWarnValueDefaultValue
105 + ) {
106 + warning(
107 + false,
108 + '%s contains an input of type %s with both value and defaultValue props. ' +
109 + 'Input elements must be either controlled or uncontrolled ' +
110 + '(specify either the value prop, or the defaultValue prop, but not ' +
111 + 'both). Decide between using a controlled or uncontrolled input ' +
112 + 'element and remove one of these props. More info: ' +
113 + 'https://fb.me/react-controlled-components',
114 + owner && owner.getName() || 'A component',
115 + props.type
116 + );
117 + didWarnValueDefaultValue = true;
118 + }
119 + }
120 +
121 + var defaultValue = props.defaultValue;
122 + inst._wrapperState = {
123 + initialChecked: props.checked != null ? props.checked : props.defaultChecked,
124 + initialValue: props.value != null ? props.value : defaultValue,
125 + listeners: null,
126 + };
127 +
128 + if (__DEV__) {
129 + inst._wrapperState.controlled = isControlled(props);
130 + }
131 + },
132 +
133 + updateWrapper: function(inst) {
134 + var props = inst._currentElement.props;
135 +
136 + if (__DEV__) {
137 + var controlled = isControlled(props);
138 + var owner = inst._currentElement._owner;
139 +
140 + if (!inst._wrapperState.controlled && controlled && !didWarnUncontrolledToControlled) {
141 + warning(
142 + false,
143 + '%s is changing an uncontrolled input of type %s to be controlled. ' +
144 + 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' +
145 + 'Decide between using a controlled or uncontrolled input ' +
146 + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components',
147 + owner && owner.getName() || 'A component',
148 + props.type
149 + );
150 + didWarnUncontrolledToControlled = true;
151 + }
152 + if (inst._wrapperState.controlled && !controlled && !didWarnControlledToUncontrolled) {
153 + warning(
154 + false,
155 + '%s is changing a controlled input of type %s to be uncontrolled. ' +
156 + 'Input elements should not switch from controlled to uncontrolled (or vice versa). ' +
157 + 'Decide between using a controlled or uncontrolled input ' +
158 + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components',
159 + owner && owner.getName() || 'A component',
160 + props.type
161 + );
162 + didWarnControlledToUncontrolled = true;
163 + }
164 + }
165 +
166 + var checked = props.checked;
167 + if (checked != null) {
168 + DOMPropertyOperations.setValueForProperty(
169 + ReactDOMComponentTree.getNodeFromInstance(inst),
170 + 'checked',
171 + checked || false
172 + );
173 + }
174 +
175 + var node = ReactDOMComponentTree.getNodeFromInstance(inst);
176 + var value = props.value;
177 + if (value != null) {
178 +
179 + // Cast `value` to a string to ensure the value is set correctly. While
180 + // browsers typically do this as necessary, jsdom doesn't.
181 + var newValue = '' + value;
182 +
183 + // To avoid side effects (such as losing text selection), only set value if changed
184 + if (newValue !== node.value) {
185 + node.value = newValue;
186 + }
187 + } else {
188 + if (props.value == null && props.defaultValue != null) {
189 + // In Chrome, assigning defaultValue to certain input types triggers input validation.
190 + // For number inputs, the display value loses trailing decimal points. For email inputs,
191 + // Chrome raises "The specified value <x> is not a valid email address".
192 + //
193 + // Here we check to see if the defaultValue has actually changed, avoiding these problems
194 + // when the user is inputting text
195 + //
196 + // https://github.com/facebook/react/issues/7253
197 + if (node.defaultValue !== '' + props.defaultValue) {
198 + node.defaultValue = '' + props.defaultValue;
199 + }
200 + }
201 + if (props.checked == null && props.defaultChecked != null) {
202 + node.defaultChecked = !!props.defaultChecked;
203 + }
204 + }
205 + },
206 +
207 + postMountWrapper: function(inst) {
208 + var props = inst._currentElement.props;
209 +
210 + // This is in postMount because we need access to the DOM node, which is not
211 + // available until after the component has mounted.
212 + var node = ReactDOMComponentTree.getNodeFromInstance(inst);
213 +
214 + // Detach value from defaultValue. We won't do anything if we're working on
215 + // submit or reset inputs as those values & defaultValues are linked. They
216 + // are not resetable nodes so this operation doesn't matter and actually
217 + // removes browser-default values (eg "Submit Query") when no value is
218 + // provided.
219 +
220 + switch (props.type) {
221 + case 'submit':
222 + case 'reset':
223 + break;
224 + case 'color':
225 + case 'date':
226 + case 'datetime':
227 + case 'datetime-local':
228 + case 'month':
229 + case 'time':
230 + case 'week':
231 + // This fixes the no-show issue on iOS Safari and Android Chrome:
232 + // https://github.com/facebook/react/issues/7233
233 + node.value = '';
234 + node.value = node.defaultValue;
235 + break;
236 + default:
237 + node.value = node.value;
238 + break;
239 + }
240 +
241 + // Normally, we'd just do `node.checked = node.checked` upon initial mount, less this bug
242 + // this is needed to work around a chrome bug where setting defaultChecked
243 + // will sometimes influence the value of checked (even after detachment).
244 + // Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=608416
245 + // We need to temporarily unset name to avoid disrupting radio button groups.
246 + var name = node.name;
247 + if (name !== '') {
248 + node.name = '';
249 + }
250 + node.defaultChecked = !node.defaultChecked;
251 + node.defaultChecked = !node.defaultChecked;
252 + if (name !== '') {
253 + node.name = name;
254 + }
255 + },
256 +
257 + restoreControlledState: function(inst) {
258 + if (inst._rootNodeID) {
259 + // DOM component is still mounted; update
260 + ReactDOMInput.updateWrapper(inst);
261 + }
262 + var props = inst._currentElement.props;
263 + updateNamedCousins(inst, props);
264 + },
265 +};
266 +
267 +function updateNamedCousins(thisInstance, props) {
268 + var name = props.name;
269 + if (props.type === 'radio' && name != null) {
270 + var rootNode = ReactDOMComponentTree.getNodeFromInstance(thisInstance);
271 + var queryRoot = rootNode;
272 +
273 + while (queryRoot.parentNode) {
274 + queryRoot = queryRoot.parentNode;
275 + }
276 +
277 + // If `rootNode.form` was non-null, then we could try `form.elements`,
278 + // but that sometimes behaves strangely in IE8. We could also try using
279 + // `form.getElementsByName`, but that will only return direct children
280 + // and won't include inputs that use the HTML5 `form=` attribute. Since
281 + // the input might not even be in a form. It might not even be in the
282 + // document. Let's just use the local `querySelectorAll` to ensure we don't
283 + // miss anything.
284 + var group = queryRoot.querySelectorAll(
285 + 'input[name=' + JSON.stringify('' + name) + '][type="radio"]');
286 +
287 + for (var i = 0; i < group.length; i++) {
288 + var otherNode = group[i];
289 + if (otherNode === rootNode ||
290 + otherNode.form !== rootNode.form) {
291 + continue;
292 + }
293 + // This will throw if radio buttons rendered by different copies of React
294 + // and the same name are rendered into the same form (same as #1939).
295 + // That's probably okay; we don't support it just as we don't support
296 + // mixing React radio buttons with non-React ones.
297 + var otherInstance = ReactDOMComponentTree.getInstanceFromNode(otherNode);
298 + invariant(
299 + otherInstance,
300 + 'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
301 + 'same `name` is not supported.'
302 + );
303 + // If this is a controlled radio button group, forcing the input that
304 + // was previously checked to update will cause it to be come re-checked
305 + // as appropriate.
306 + if (otherInstance._rootNodeID) {
307 + ReactDOMInput.updateWrapper(otherInstance);
308 + }
309 + }
310 + }
311 +}
312 +
313 +module.exports = ReactDOMInput;
src/renderers/dom/fiber/wrappers/ReactDOMFiberOption.js new
+128
@@ -0,0 +1,128 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactDOMFiberOption
10 + */
11 +
12 +'use strict';
13 +
14 +var React = require('React');
15 +var ReactDOMComponentTree = require('ReactDOMComponentTree');
16 +var ReactDOMSelect = require('ReactDOMSelect');
17 +
18 +var warning = require('warning');
19 +var didWarnInvalidOptionChildren = false;
20 +
21 +function flattenChildren(children) {
22 + var content = '';
23 +
24 + // Flatten children and warn if they aren't strings or numbers;
25 + // invalid types are ignored.
26 + React.Children.forEach(children, function(child) {
27 + if (child == null) {
28 + return;
29 + }
30 + if (typeof child === 'string' || typeof child === 'number') {
31 + content += child;
32 + } else if (!didWarnInvalidOptionChildren) {
33 + didWarnInvalidOptionChildren = true;
34 + warning(
35 + false,
36 + 'Only strings and numbers are supported as <option> children.'
37 + );
38 + }
39 + });
40 +
41 + return content;
42 +}
43 +
44 +/**
45 + * Implements an <option> host component that warns when `selected` is set.
46 + */
47 +var ReactDOMOption = {
48 + mountWrapper: function(inst, props, hostParent) {
49 + // TODO (yungsters): Remove support for `selected` in <option>.
50 + if (__DEV__) {
51 + warning(
52 + props.selected == null,
53 + 'Use the `defaultValue` or `value` props on <select> instead of ' +
54 + 'setting `selected` on <option>.'
55 + );
56 + }
57 +
58 + // Look up whether this option is 'selected'
59 + var selectValue = null;
60 + if (hostParent != null) {
61 + var selectParent = hostParent;
62 +
63 + if (selectParent._tag === 'optgroup') {
64 + selectParent = selectParent._hostParent;
65 + }
66 +
67 + if (selectParent != null && selectParent._tag === 'select') {
68 + selectValue = ReactDOMSelect.getSelectValueContext(selectParent);
69 + }
70 + }
71 +
72 + // If the value is null (e.g., no specified value or after initial mount)
73 + // or missing (e.g., for <datalist>), we don't change props.selected
74 + var selected = null;
75 + if (selectValue != null) {
76 + var value;
77 + if (props.value != null) {
78 + value = props.value + '';
79 + } else {
80 + value = flattenChildren(props.children);
81 + }
82 + selected = false;
83 + if (Array.isArray(selectValue)) {
84 + // multiple
85 + for (var i = 0; i < selectValue.length; i++) {
86 + if ('' + selectValue[i] === value) {
87 + selected = true;
88 + break;
89 + }
90 + }
91 + } else {
92 + selected = ('' + selectValue === value);
93 + }
94 + }
95 +
96 + inst._wrapperState = {selected: selected};
97 + },
98 +
99 + postMountWrapper: function(inst) {
100 + // value="" should make a value attribute (#6219)
101 + var props = inst._currentElement.props;
102 + if (props.value != null) {
103 + var node = ReactDOMComponentTree.getNodeFromInstance(inst);
104 + node.setAttribute('value', props.value);
105 + }
106 + },
107 +
108 + getHostProps: function(inst, props) {
109 + var hostProps = Object.assign({selected: undefined, children: undefined}, props);
110 +
111 + // Read state only from initial mount because <select> updates value
112 + // manually; we need the initial state only for server rendering
113 + if (inst._wrapperState.selected != null) {
114 + hostProps.selected = inst._wrapperState.selected;
115 + }
116 +
117 + var content = flattenChildren(props.children);
118 +
119 + if (content) {
120 + hostProps.children = content;
121 + }
122 +
123 + return hostProps;
124 + },
125 +
126 +};
127 +
128 +module.exports = ReactDOMOption;
src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js new
+200
@@ -0,0 +1,200 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactDOMFiberSelect
10 + */
11 +
12 +'use strict';
13 +
14 +var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes');
15 +var ReactDOMComponentTree = require('ReactDOMComponentTree');
16 +
17 +var warning = require('warning');
18 +
19 +var didWarnValueDefaultValue = false;
20 +
21 +function getDeclarationErrorAddendum(owner) {
22 + if (owner) {
23 + var name = owner.getName();
24 + if (name) {
25 + return ' Check the render method of `' + name + '`.';
26 + }
27 + }
28 + return '';
29 +}
30 +
31 +var valuePropNames = ['value', 'defaultValue'];
32 +
33 +/**
34 + * Validation function for `value` and `defaultValue`.
35 + * @private
36 + */
37 +function checkSelectPropTypes(inst, props) {
38 + var owner = inst._currentElement._owner;
39 + ReactControlledValuePropTypes.checkPropTypes(
40 + 'select',
41 + props,
42 + owner
43 + );
44 +
45 + for (var i = 0; i < valuePropNames.length; i++) {
46 + var propName = valuePropNames[i];
47 + if (props[propName] == null) {
48 + continue;
49 + }
50 + var isArray = Array.isArray(props[propName]);
51 + if (props.multiple && !isArray) {
52 + warning(
53 + false,
54 + 'The `%s` prop supplied to <select> must be an array if ' +
55 + '`multiple` is true.%s',
56 + propName,
57 + getDeclarationErrorAddendum(owner)
58 + );
59 + } else if (!props.multiple && isArray) {
60 + warning(
61 + false,
62 + 'The `%s` prop supplied to <select> must be a scalar ' +
63 + 'value if `multiple` is false.%s',
64 + propName,
65 + getDeclarationErrorAddendum(owner)
66 + );
67 + }
68 + }
69 +}
70 +
71 +/**
72 + * @param {ReactDOMComponent} inst
73 + * @param {boolean} multiple
74 + * @param {*} propValue A stringable (with `multiple`, a list of stringables).
75 + * @private
76 + */
77 +function updateOptions(inst, multiple, propValue) {
78 + var options = ReactDOMComponentTree.getNodeFromInstance(inst).options;
79 +
80 + if (multiple) {
81 + let selectedValue = {};
82 + for (let i = 0; i < propValue.length; i++) {
83 + selectedValue['' + propValue[i]] = true;
84 + }
85 + for (let i = 0; i < options.length; i++) {
86 + var selected = selectedValue.hasOwnProperty(options[i].value);
87 + if (options[i].selected !== selected) {
88 + options[i].selected = selected;
89 + }
90 + }
91 + } else {
92 + // Do not set `select.value` as exact behavior isn't consistent across all
93 + // browsers for all cases.
94 + let selectedValue = '' + propValue;
95 + for (let i = 0; i < options.length; i++) {
96 + if (options[i].value === selectedValue) {
97 + options[i].selected = true;
98 + return;
99 + }
100 + }
101 + if (options.length) {
102 + options[0].selected = true;
103 + }
104 + }
105 +}
106 +
107 +/**
108 + * Implements a <select> host component that allows optionally setting the
109 + * props `value` and `defaultValue`. If `multiple` is false, the prop must be a
110 + * stringable. If `multiple` is true, the prop must be an array of stringables.
111 + *
112 + * If `value` is not supplied (or null/undefined), user actions that change the
113 + * selected option will trigger updates to the rendered options.
114 + *
115 + * If it is supplied (and not null/undefined), the rendered options will not
116 + * update in response to user actions. Instead, the `value` prop must change in
117 + * order for the rendered options to update.
118 + *
119 + * If `defaultValue` is provided, any options with the supplied values will be
120 + * selected.
121 + */
122 +var ReactDOMSelect = {
123 + getHostProps: function(inst, props) {
124 + return Object.assign({}, props, {
125 + value: undefined,
126 + });
127 + },
128 +
129 + mountWrapper: function(inst, props) {
130 + if (__DEV__) {
131 + checkSelectPropTypes(inst, props);
132 + }
133 +
134 + var value = props.value;
135 + inst._wrapperState = {
136 + initialValue: value != null ? value : props.defaultValue,
137 + listeners: null,
138 + wasMultiple: Boolean(props.multiple),
139 + };
140 +
141 + if (
142 + props.value !== undefined &&
143 + props.defaultValue !== undefined &&
144 + !didWarnValueDefaultValue
145 + ) {
146 + warning(
147 + false,
148 + 'Select elements must be either controlled or uncontrolled ' +
149 + '(specify either the value prop, or the defaultValue prop, but not ' +
150 + 'both). Decide between using a controlled or uncontrolled select ' +
151 + 'element and remove one of these props. More info: ' +
152 + 'https://fb.me/react-controlled-components'
153 + );
154 + didWarnValueDefaultValue = true;
155 + }
156 + },
157 +
158 + getSelectValueContext: function(inst) {
159 + // ReactDOMOption looks at this initial value so the initial generated
160 + // markup has correct `selected` attributes
161 + return inst._wrapperState.initialValue;
162 + },
163 +
164 + postUpdateWrapper: function(inst) {
165 + var props = inst._currentElement.props;
166 +
167 + // After the initial mount, we control selected-ness manually so don't pass
168 + // this value down
169 + inst._wrapperState.initialValue = undefined;
170 +
171 + var wasMultiple = inst._wrapperState.wasMultiple;
172 + inst._wrapperState.wasMultiple = Boolean(props.multiple);
173 +
174 + var value = props.value;
175 + if (value != null) {
176 + updateOptions(inst, Boolean(props.multiple), value);
177 + } else if (wasMultiple !== Boolean(props.multiple)) {
178 + // For simplicity, reapply `defaultValue` if `multiple` is toggled.
179 + if (props.defaultValue != null) {
180 + updateOptions(inst, Boolean(props.multiple), props.defaultValue);
181 + } else {
182 + // Revert the select back to its default unselected state.
183 + updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
184 + }
185 + }
186 + },
187 +
188 + restoreControlledState: function(inst) {
189 + if (inst._rootNodeID) {
190 + var props = inst._currentElement.props;
191 + var value = props.value;
192 +
193 + if (value != null) {
194 + updateOptions(inst, Boolean(props.multiple), value);
195 + }
196 + }
197 + },
198 +};
199 +
200 +module.exports = ReactDOMSelect;
src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js new
+172
@@ -0,0 +1,172 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule ReactDOMFiberTextarea
10 + */
11 +
12 +'use strict';
13 +
14 +var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes');
15 +var ReactDOMComponentTree = require('ReactDOMComponentTree');
16 +
17 +var invariant = require('invariant');
18 +var warning = require('warning');
19 +
20 +var didWarnValDefaultVal = false;
21 +
22 +/**
23 + * Implements a <textarea> host component that allows setting `value`, and
24 + * `defaultValue`. This differs from the traditional DOM API because value is
25 + * usually set as PCDATA children.
26 + *
27 + * If `value` is not supplied (or null/undefined), user actions that affect the
28 + * value will trigger updates to the element.
29 + *
30 + * If `value` is supplied (and not null/undefined), the rendered element will
31 + * not trigger updates to the element. Instead, the `value` prop must change in
32 + * order for the rendered element to be updated.
33 + *
34 + * The rendered element will be initialized with an empty value, the prop
35 + * `defaultValue` if specified, or the children content (deprecated).
36 + */
37 +var ReactDOMTextarea = {
38 + getHostProps: function(inst, props) {
39 + invariant(
40 + props.dangerouslySetInnerHTML == null,
41 + '`dangerouslySetInnerHTML` does not make sense on <textarea>.'
42 + );
43 +
44 + // Always set children to the same thing. In IE9, the selection range will
45 + // get reset if `textContent` is mutated. We could add a check in setTextContent
46 + // to only set the value if/when the value differs from the node value (which would
47 + // completely solve this IE9 bug), but Sebastian+Ben seemed to like this solution.
48 + // The value can be a boolean or object so that's why it's forced to be a string.
49 + var hostProps = Object.assign({}, props, {
50 + value: undefined,
51 + defaultValue: undefined,
52 + children: '' + inst._wrapperState.initialValue,
53 + });
54 +
55 + return hostProps;
56 + },
57 +
58 + mountWrapper: function(inst, props) {
59 + if (__DEV__) {
60 + ReactControlledValuePropTypes.checkPropTypes(
61 + 'textarea',
62 + props,
63 + inst._currentElement._owner
64 + );
65 + if (
66 + props.value !== undefined &&
67 + props.defaultValue !== undefined &&
68 + !didWarnValDefaultVal
69 + ) {
70 + warning(
71 + false,
72 + 'Textarea elements must be either controlled or uncontrolled ' +
73 + '(specify either the value prop, or the defaultValue prop, but not ' +
74 + 'both). Decide between using a controlled or uncontrolled textarea ' +
75 + 'and remove one of these props. More info: ' +
76 + 'https://fb.me/react-controlled-components'
77 + );
78 + didWarnValDefaultVal = true;
79 + }
80 + }
81 +
82 +
83 + var value = props.value;
84 + var initialValue = value;
85 +
86 + // Only bother fetching default value if we're going to use it
87 + if (value == null) {
88 + var defaultValue = props.defaultValue;
89 + // TODO (yungsters): Remove support for children content in <textarea>.
90 + var children = props.children;
91 + if (children != null) {
92 + if (__DEV__) {
93 + warning(
94 + false,
95 + 'Use the `defaultValue` or `value` props instead of setting ' +
96 + 'children on <textarea>.'
97 + );
98 + }
99 + invariant(
100 + defaultValue == null,
101 + 'If you supply `defaultValue` on a <textarea>, do not pass children.'
102 + );
103 + if (Array.isArray(children)) {
104 + invariant(
105 + children.length <= 1,
106 + '<textarea> can only have at most one child.'
107 + );
108 + children = children[0];
109 + }
110 +
111 + defaultValue = '' + children;
112 + }
113 + if (defaultValue == null) {
114 + defaultValue = '';
115 + }
116 + initialValue = defaultValue;
117 + }
118 +
119 + inst._wrapperState = {
120 + initialValue: '' + initialValue,
121 + listeners: null,
122 + };
123 + },
124 +
125 + updateWrapper: function(inst) {
126 + var props = inst._currentElement.props;
127 +
128 + var node = ReactDOMComponentTree.getNodeFromInstance(inst);
129 + var value = props.value;
130 + if (value != null) {
131 + // Cast `value` to a string to ensure the value is set correctly. While
132 + // browsers typically do this as necessary, jsdom doesn't.
133 + var newValue = '' + value;
134 +
135 + // To avoid side effects (such as losing text selection), only set value if changed
136 + if (newValue !== node.value) {
137 + node.value = newValue;
138 + }
139 + if (props.defaultValue == null) {
140 + node.defaultValue = newValue;
141 + }
142 + }
143 + if (props.defaultValue != null) {
144 + node.defaultValue = props.defaultValue;
145 + }
146 + },
147 +
148 + postMountWrapper: function(inst) {
149 + // This is in postMount because we need access to the DOM node, which is not
150 + // available until after the component has mounted.
151 + var node = ReactDOMComponentTree.getNodeFromInstance(inst);
152 + var textContent = node.textContent;
153 +
154 + // Only set node.value if textContent is equal to the expected
155 + // initial value. In IE10/IE11 there is a bug where the placeholder attribute
156 + // will populate textContent as well.
157 + // https://developer.microsoft.com/microsoft-edge/platform/issues/101525/
158 + if (textContent === inst._wrapperState.initialValue) {
159 + node.value = textContent;
160 + }
161 + },
162 +
163 + restoreControlledState: function(inst) {
164 + if (inst._rootNodeID) {
165 + // DOM component is still mounted; update
166 + ReactDOMTextarea.updateWrapper(inst);
167 + }
168 + },
169 +
170 +};
171 +
172 +module.exports = ReactDOMTextarea;