From 864ae8fa987bebfc43ea87476a03a44ac010ebce Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Tue, 27 Jun 2017 17:22:07 -0700 Subject: [PATCH] Support comment node as a mount point (#9835) This means you don't need an extra wrapper div for each component root you need. Rendered stuff is inserted before the comment you pass in. --- scripts/fiber/tests-passing.txt | 1 + src/renderers/dom/fiber/ReactDOMFiberEntry.js | 56 ++++++++++++++----- .../dom/shared/__tests__/ReactMount-test.js | 45 +++++++++++++++ 3 files changed, 89 insertions(+), 13 deletions(-) diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 04357a1244..2e36948c29 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1433,6 +1433,7 @@ src/renderers/dom/shared/__tests__/ReactMount-test.js * should warn if the unmounted node was rendered by another copy of React * passes the correct callback context * initial mount is sync inside batchedUpdates, but task work is deferred until the end of the batch +* renders at a comment node src/renderers/dom/shared/__tests__/ReactMountDestruction-test.js * should destroy a react root upon request diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 7997326f9a..3a5196803b 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -33,6 +33,8 @@ var {isValidElement} = require('react'); var {injectInternals} = require('ReactFiberDevToolsHook'); var { ELEMENT_NODE, + TEXT_NODE, + COMMENT_NODE, DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, } = require('HTMLNodeType'); @@ -99,7 +101,9 @@ function isValidContainer(node) { return !!(node && (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_NODE || - node.nodeType === DOCUMENT_FRAGMENT_NODE)); + node.nodeType === DOCUMENT_FRAGMENT_NODE || + (node.nodeType === COMMENT_NODE && + node.nodeValue === ' react-mount-point-unstable '))); } function getReactRootElementInContainer(container: any) { @@ -141,8 +145,11 @@ var DOMRenderer = ReactFiberReconciler({ let root = (rootContainerInstance: any).documentElement; namespace = root ? root.namespaceURI : getChildNamespace(null, ''); } else { - const ownNamespace = (rootContainerInstance: any).namespaceURI || null; - type = (rootContainerInstance: any).tagName; + const container: any = rootContainerInstance.nodeType === COMMENT_NODE + ? rootContainerInstance.parentNode + : rootContainerInstance; + const ownNamespace = container.namespaceURI || null; + type = container.tagName; namespace = getChildNamespace(ownNamespace, type); } if (__DEV__) { @@ -354,7 +361,11 @@ var DOMRenderer = ReactFiberReconciler({ container: Container, child: Instance | TextInstance, ): void { - container.appendChild(child); + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).insertBefore(child, container); + } else { + container.appendChild(child); + } }, insertBefore( @@ -370,7 +381,11 @@ var DOMRenderer = ReactFiberReconciler({ child: Instance | TextInstance, beforeChild: Instance | TextInstance, ): void { - container.insertBefore(child, beforeChild); + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).insertBefore(child, beforeChild); + } else { + container.insertBefore(child, beforeChild); + } }, removeChild(parentInstance: Instance, child: Instance | TextInstance): void { @@ -381,7 +396,11 @@ var DOMRenderer = ReactFiberReconciler({ container: Container, child: Instance | TextInstance, ): void { - container.removeChild(child); + if (container.nodeType === COMMENT_NODE) { + (container.parentNode: any).removeChild(child); + } else { + container.removeChild(child); + } }, canHydrateInstance( @@ -389,7 +408,10 @@ var DOMRenderer = ReactFiberReconciler({ type: string, props: Props, ): boolean { - return instance.nodeType === 1 && type === instance.nodeName.toLowerCase(); + return ( + instance.nodeType === ELEMENT_NODE && + type === instance.nodeName.toLowerCase() + ); }, canHydrateTextInstance( @@ -400,7 +422,7 @@ var DOMRenderer = ReactFiberReconciler({ // Empty strings are not parsed by HTML so there won't be a correct match here. return false; } - return instance.nodeType === 3; + return instance.nodeType === TEXT_NODE; }, getNextHydratableSibling( @@ -408,7 +430,11 @@ var DOMRenderer = ReactFiberReconciler({ ): null | Instance | TextInstance { let node = instance.nextSibling; // Skip non-hydratable nodes. - while (node && node.nodeType !== 1 && node.nodeType !== 3) { + while ( + node && + node.nodeType !== ELEMENT_NODE && + node.nodeType !== TEXT_NODE + ) { node = node.nextSibling; } return (node: any); @@ -419,7 +445,11 @@ var DOMRenderer = ReactFiberReconciler({ ): null | Instance | TextInstance { let next = parentInstance.firstChild; // Skip non-hydratable nodes. - while (next && next.nodeType !== 1 && next.nodeType !== 3) { + while ( + next && + next.nodeType !== ELEMENT_NODE && + next.nodeType !== TEXT_NODE + ) { next = next.nextSibling; } return (next: any); @@ -483,9 +513,9 @@ function renderSubtreeIntoContainer( ); warning( - container.nodeType !== 1 || - !container.tagName || - container.tagName.toUpperCase() !== 'BODY', + container.nodeType !== ELEMENT_NODE || + !((container: any): Element).tagName || + ((container: any): Element).tagName.toUpperCase() !== 'BODY', 'render(): Rendering components directly into document.body is ' + 'discouraged, since its children are often manipulated by third-party ' + 'scripts and browser extensions. This may lead to subtle ' + diff --git a/src/renderers/dom/shared/__tests__/ReactMount-test.js b/src/renderers/dom/shared/__tests__/ReactMount-test.js index a60ba17fe4..077070008a 100644 --- a/src/renderers/dom/shared/__tests__/ReactMount-test.js +++ b/src/renderers/dom/shared/__tests__/ReactMount-test.js @@ -11,6 +11,10 @@ 'use strict'; +const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + +const invariant = require('invariant'); + var React; var ReactDOM; var ReactDOMServer; @@ -345,4 +349,45 @@ describe('ReactMount', () => { expect(container1.textContent).toEqual('2'); expect(container2.textContent).toEqual('a!'); }); + + if (ReactDOMFeatureFlags.useFiber) { + describe('mount point is a comment node', () => { + let containerDiv; + let mountPoint; + + beforeEach(() => { + const ReactFeatureFlags = require('ReactFeatureFlags'); + ReactFeatureFlags.disableNewFiberFeatures = false; + + containerDiv = document.createElement('div'); + containerDiv.innerHTML = 'AB'; + mountPoint = containerDiv.childNodes[1]; + invariant(mountPoint.nodeType === 8, 'Expected comment'); + }); + + it('renders at a comment node', () => { + function Char(props) { + return props.children; + } + function list(chars) { + return chars.split('').map(c => {c}); + } + + ReactDOM.render(list('aeiou'), mountPoint); + expect(containerDiv.innerHTML).toBe( + 'AaeiouB', + ); + + ReactDOM.render(list('yea'), mountPoint); + expect(containerDiv.innerHTML).toBe( + 'AyeaB', + ); + + ReactDOM.render(list(''), mountPoint); + expect(containerDiv.innerHTML).toBe( + 'AB', + ); + }); + }); + } });