@samitouri / QOS-React / commits / 7cd26b9c71

Manage children inside DOM properties

Fiber manages children separately so we don't need to do it here. However, we special case the text content children and dangerouslySetInnerHTML. This reveals a bug that we currently don't handle the case where we switch from dangerouslySetInnerHTML or text children to element children, because child insertions are handled before the parent updates. We could possibly handle this case by removing all nodes before the first host child but that is a bit unfortunate.

Sebastian Markbage committed Nov 15, 2016 at 19:02 UTC 7cd26b9c7172465981b70ea6bf0bb9b09ce8b715
1 file changed +46 -114
src/renderers/dom/fiber/ReactDOMFiberComponent.js
+46 -114
@@ -16,7 +16,6 @@
16
17 var AutoFocusUtils = require('AutoFocusUtils');
18 var CSSPropertyOperations = require('CSSPropertyOperations');
19 -var DOMLazyTree = require('DOMLazyTree');
19 var DOMNamespaces = require('DOMNamespaces');
20 var DOMProperty = require('DOMProperty');
21 var DOMPropertyOperations = require('DOMPropertyOperations');
@@ -27,13 +26,14 @@ var ReactDOMFiberInput = require('ReactDOMFiberInput');
26 var ReactDOMFiberOption = require('ReactDOMFiberOption');
27 var ReactDOMFiberSelect = require('ReactDOMFiberSelect');
28 var ReactDOMFiberTextarea = require('ReactDOMFiberTextarea');
30 -var ReactMultiChild = require('ReactMultiChild');
29 var ReactServerRenderingTransaction = require('ReactServerRenderingTransaction');
30
31 var emptyFunction = require('emptyFunction');
32 var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
33 var invariant = require('invariant');
34 var isEventSupported = require('isEventSupported');
35 +var setInnerHTML = require('setInnerHTML');
36 +var setTextContent = require('setTextContent');
37 var shallowEqual = require('shallowEqual');
38 var inputValueTracking = require('inputValueTracking');
39 var warning = require('warning');
@@ -46,13 +46,11 @@ var registrationNameModules = EventPluginRegistry.registrationNameModules;
46 // For quickly matching children type, to test if can be treated as content.
47 var CONTENT_TYPES = {'string': true, 'number': true};
48
49 +var DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
50 +var SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
51 +var CHILDREN = 'children';
52 var STYLE = 'style';
53 var HTML = '__html';
51 -var RESERVED_PROPS = {
52 - children: null,
53 - dangerouslySetInnerHTML: null,
54 - suppressContentEditableWarning: null,
55 -};
54
55 // Node type for document fragments (Node.DOCUMENT_FRAGMENT_NODE).
56 var DOC_FRAGMENT_TYPE = 11;
@@ -363,39 +361,6 @@ function isCustomComponent(tagName, props) {
361 return tagName.indexOf('-') >= 0 || props.is != null;
362 }
363
366 -function createInitialChildren(workInProgress, transaction, props, context, lazyTree) {
367 - // Intentional use of != to avoid catching zero/false.
368 - var innerHTML = props.dangerouslySetInnerHTML;
369 - if (innerHTML != null) {
370 - if (innerHTML.__html != null) {
371 - DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
372 - }
373 - } else {
374 - var contentToUse =
375 - CONTENT_TYPES[typeof props.children] ? props.children : null;
376 - var childrenToUse = contentToUse != null ? null : props.children;
377 - // TODO: Validate that text is allowed as a child of this node
378 - if (contentToUse != null) {
379 - // Avoid setting textContent when the text is empty. In IE11 setting
380 - // textContent on a text area will cause the placeholder to not
381 - // show within the textarea until it has been focused and blurred again.
382 - // https://github.com/facebook/react/issues/6731#issuecomment-254874553
383 - if (contentToUse !== '') {
384 - DOMLazyTree.queueText(lazyTree, contentToUse);
385 - }
386 - } else if (childrenToUse != null) {
387 - var mountImages = workInProgress.mountChildren(
388 - childrenToUse,
389 - transaction,
390 - context
391 - );
392 - for (var i = 0; i < mountImages.length; i++) {
393 - DOMLazyTree.queueChild(lazyTree, mountImages[i]);
394 - }
395 - }
396 - }
397 -}
398 -
364 /**
365 * Reconciles the properties by detecting differences in property values and
366 * updating the DOM as necessary. This function is probably the single most
@@ -436,15 +401,20 @@ function updateDOMProperties(
401 styleUpdates[styleName] = '';
402 }
403 }
404 + } else if (propKey === DANGEROUSLY_SET_INNER_HTML ||
405 + propKey === CHILDREN) {
406 + // TODO: Clear innerHTML. This is currently broken in Fiber because we are
407 + // too late to clear everything at this point because new children have
408 + // already been inserted.
409 + } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
410 + // Noop
411 } else if (registrationNameModules.hasOwnProperty(propKey)) {
412 // Do nothing for deleted listeners.
413 } else if (isCustomComponent(workInProgress._tag, lastProps)) {
442 - if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
443 - DOMPropertyOperations.deleteValueForAttribute(
444 - getNode(workInProgress),
445 - propKey
446 - );
447 - }
414 + DOMPropertyOperations.deleteValueForAttribute(
415 + getNode(workInProgress),
416 + propKey
417 + );
418 } else if (
419 DOMProperty.properties[propKey] ||
420 DOMProperty.isCustomAttribute(propKey)) {
@@ -489,18 +459,40 @@ function updateDOMProperties(
459 // Relies on `updateStylesByID` not mutating `styleUpdates`.
460 styleUpdates = nextProp;
461 }
462 + } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
463 + if (lastProp && nextProp) {
464 + var lastHtml = lastProp[HTML];
465 + var nextHtml = nextProp[HTML];
466 + if (lastHtml !== nextHtml) {
467 + if (nextHtml == null) {
468 + // TODO: It might be too late to clear this if we have children
469 + // inserted already.
470 + } else {
471 + setInnerHTML(getNode(workInProgress), '' + nextHtml);
472 + }
473 + }
474 + } else if (nextProp) {
475 + var nextHtml = nextProp[HTML];
476 + setInnerHTML(getNode(workInProgress), nextProp);
477 + }
478 + } else if (propKey === CHILDREN) {
479 + if (typeof nextProp === 'string') {
480 + setTextContent(getNode(workInProgress), nextProp);
481 + } else if (typeof nextProp === 'number') {
482 + setTextContent(getNode(workInProgress), '' + nextProp);
483 + }
484 + } else if (propKey === SUPPRESS_CONTENT_EDITABLE_WARNING) {
485 + // Noop
486 } else if (registrationNameModules.hasOwnProperty(propKey)) {
487 if (nextProp) {
488 ensureListeningTo(workInProgress, propKey, transaction);
489 }
490 } else if (isCustomComponentTag) {
497 - if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
498 - DOMPropertyOperations.setValueForAttribute(
499 - getNode(workInProgress),
500 - propKey,
501 - nextProp
502 - );
503 - }
491 + DOMPropertyOperations.setValueForAttribute(
492 + getNode(workInProgress),
493 + propKey,
494 + nextProp
495 + );
496 } else if (
497 DOMProperty.properties[propKey] ||
498 DOMProperty.isCustomAttribute(propKey)) {
@@ -524,55 +516,6 @@ function updateDOMProperties(
516 }
517 }
518
527 -/**
528 - * Reconciles the children with the various properties that affect the
529 - * children content.
530 - *
531 - * @param {object} lastProps
532 - * @param {object} nextProps
533 - * @param {ReactReconcileTransaction} transaction
534 - * @param {object} context
535 - */
536 -function updateDOMChildren(workInProgress, lastProps, nextProps, transaction, context) {
537 - var lastContent =
538 - CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
539 - var nextContent =
540 - CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
541 -
542 - var lastHtml =
543 - lastProps.dangerouslySetInnerHTML &&
544 - lastProps.dangerouslySetInnerHTML.__html;
545 - var nextHtml =
546 - nextProps.dangerouslySetInnerHTML &&
547 - nextProps.dangerouslySetInnerHTML.__html;
548 -
549 - // Note the use of `!=` which checks for null or undefined.
550 - var lastChildren = lastContent != null ? null : lastProps.children;
551 - var nextChildren = nextContent != null ? null : nextProps.children;
552 -
553 - // If we're switching from children to content/html or vice versa, remove
554 - // the old content
555 - var lastHasContentOrHtml = lastContent != null || lastHtml != null;
556 - var nextHasContentOrHtml = nextContent != null || nextHtml != null;
557 - if (lastChildren != null && nextChildren == null) {
558 - workInProgress.updateChildren(null, transaction, context);
559 - } else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
560 - workInProgress.updateTextContent('');
561 - }
562 -
563 - if (nextContent != null) {
564 - if (lastContent !== nextContent) {
565 - workInProgress.updateTextContent('' + nextContent);
566 - }
567 - } else if (nextHtml != null) {
568 - if (lastHtml !== nextHtml) {
569 - workInProgress.updateMarkup('' + nextHtml);
570 - }
571 - } else if (nextChildren != null) {
572 - workInProgress.updateChildren(nextChildren, transaction, context);
573 - }
574 -}
575 -
519 var ReactDOMFiberComponent = {
520
521
@@ -716,9 +659,6 @@ var ReactDOMFiberComponent = {
659 DOMPropertyOperations.setAttributeForRoot(el);
660 }
661 updateDOMProperties(workInProgress, null, props, transaction, isCustomComponentTag);
719 - var lazyTree = DOMLazyTree(el);
720 - createInitialChildren(workInProgress, transaction, props, context, lazyTree);
721 - mountImage = lazyTree;
662
663 switch (workInProgress._tag) {
664 case 'input':
@@ -777,7 +717,7 @@ var ReactDOMFiberComponent = {
717 break;
718 }
719
780 - return mountImage;
720 + return el;
721 },
722
723
@@ -827,13 +767,6 @@ var ReactDOMFiberComponent = {
767 assertValidProps(workInProgress, nextProps);
768 var isCustomComponentTag = isCustomComponent(workInProgress._tag, nextProps);
769 updateDOMProperties(workInProgress, lastProps, nextProps, transaction, isCustomComponentTag);
830 - updateDOMChildren(
831 - workInProgress,
832 - lastProps,
833 - nextProps,
834 - transaction,
835 - context
836 - );
770
771 switch (workInProgress._tag) {
772 case 'input':
@@ -901,7 +834,6 @@ var ReactDOMFiberComponent = {
834 break;
835 }
836
904 - workInProgress.unmountChildren(safely, skipLifecycle);
837 ReactDOMComponentTree.uncacheNode(workInProgress);
838 workInProgress._wrapperState = null;
839 },