diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 8084ad6f8280..63a7de9dff8e 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -120,4 +120,3 @@ src/renderers/shared/stack/reconciler/__tests__/refs-test.js src/test/__tests__/ReactTestUtils-test.js * traverses children in the correct order -* should support injected wrapper components as DOM components diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 52430274b694..cd096d16be03 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1560,6 +1560,7 @@ src/test/__tests__/ReactTestUtils-test.js * can scryRenderedDOMComponentsWithClass with TextComponent * can scryRenderedDOMComponentsWithClass with className contains \n * can scryRenderedDOMComponentsWithClass with multiple classes +* should support injected wrapper components as DOM components * should change the value of an input field * should change the value of an input field in a component * should throw when attempting to use ReactTestUtils.Simulate with shallow rendering diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index f09f9ddbe198..f2dff37cc0a7 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -13,7 +13,6 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; -import type { HostChildren } from 'ReactFiberReconciler'; import type { ReactNodeList } from 'ReactTypes'; var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); @@ -38,6 +37,8 @@ var { } = ReactDOMFiberComponent; var { precacheFiberNode } = ReactDOMComponentTree; +const DOCUMENT_NODE = 9; + ReactDOMInjection.inject(); ReactControlledComponent.injection.injectFiberControlledHostComponent( ReactDOMFiberComponent @@ -53,23 +54,6 @@ type Props = { className ?: string }; type Instance = Element; type TextInstance = Text; -function recursivelyAppendChildren(parent : Element, child : HostChildren) { - if (!child) { - return; - } - /* $FlowFixMe: Element and Text should have this property. */ - if (child.nodeType === 1 || child.nodeType === 3) { - /* $FlowFixMe: Refinement issue. I don't know how to express different. */ - parent.appendChild(child); - } else { - /* As a result of the refinement issue this type isn't known. */ - let node : any = child; - do { - recursivelyAppendChildren(parent, node.output); - } while (node = node.sibling); - } -} - let eventsEnabled : ?boolean = null; let selectionInformation : ?mixed = null; @@ -88,27 +72,28 @@ var DOMRenderer = ReactFiberReconciler({ eventsEnabled = null; }, - updateContainer(container : Container, children : HostChildren) : void { - // TODO: Containers should update similarly to other parents. - container.innerHTML = ''; - recursivelyAppendChildren(container, children); - }, - createInstance( type : string, props : Props, - children : HostChildren, internalInstanceHandle : Object ) : Instance { - const root = document.body; // HACK + const root = document.documentElement; // HACK const domElement : Instance = createElement(type, props, root); precacheFiberNode(internalInstanceHandle, domElement); - recursivelyAppendChildren(domElement, children); - setInitialProperties(domElement, type, props, root); return domElement; }, + appendInitialChild(parentInstance : Instance, child : Instance | TextInstance) : void { + parentInstance.appendChild(child); + }, + + finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void { + const root = document.documentElement; // HACK + + setInitialProperties(domElement, type, props, root); + }, + prepareUpdate( domElement : Instance, oldProps : Props, @@ -124,7 +109,7 @@ var DOMRenderer = ReactFiberReconciler({ internalInstanceHandle : Object ) : void { var type = domElement.tagName.toLowerCase(); // HACK - var root = document.body; // HACK + var root = document.documentElement; // HACK // Update the internal instance handle so that we know which props are // the current ones. precacheFiberNode(internalInstanceHandle, domElement); @@ -141,19 +126,19 @@ var DOMRenderer = ReactFiberReconciler({ textInstance.nodeValue = newText; }, - appendChild(parentInstance : Instance, child : Instance | TextInstance) : void { + appendChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void { parentInstance.appendChild(child); }, insertBefore( - parentInstance : Instance, + parentInstance : Instance | Container, child : Instance | TextInstance, beforeChild : Instance | TextInstance ) : void { parentInstance.insertBefore(child, beforeChild); }, - removeChild(parentInstance : Instance, child : Instance | TextInstance) : void { + removeChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void { parentInstance.removeChild(child); }, @@ -177,9 +162,15 @@ function warnAboutUnstableUse() { warned = true; } -function renderSubtreeIntoContainer(parentComponent : ?ReactComponent, element : ReactElement, container : DOMContainerElement, callback: ?Function) { +function renderSubtreeIntoContainer(parentComponent : ?ReactComponent, element : ReactElement, containerNode : DOMContainerElement | Document, callback: ?Function) { + let container : DOMContainerElement = + containerNode.nodeType === DOCUMENT_NODE ? (containerNode : any).documentElement : (containerNode : any); let root; if (!container._reactRootContainer) { + // First clear any existing content. + while (container.lastChild) { + container.removeChild(container.lastChild); + } root = container._reactRootContainer = DOMRenderer.mountContainer(element, container, parentComponent, callback); } else { DOMRenderer.updateContainer(element, root = container._reactRootContainer, parentComponent, callback); @@ -194,12 +185,12 @@ var ReactDOM = { return renderSubtreeIntoContainer(null, element, container, callback); }, - unstable_renderSubtreeIntoContainer(parentComponent : ReactComponent, element : ReactElement, container : DOMContainerElement, callback: ?Function) { + unstable_renderSubtreeIntoContainer(parentComponent : ReactComponent, element : ReactElement, containerNode : DOMContainerElement | Document, callback: ?Function) { invariant( parentComponent != null && ReactInstanceMap.has(parentComponent), 'parentComponent must be a valid React Component' ); - return renderSubtreeIntoContainer(parentComponent, element, container, callback); + return renderSubtreeIntoContainer(parentComponent, element, containerNode, callback); }, unmountComponentAtNode(container : DOMContainerElement) { diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 5bd32f08c7dd..9f614a465ea3 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -21,7 +21,6 @@ import type { Fiber } from 'ReactFiber'; import type { UpdateQueue } from 'ReactFiberUpdateQueue'; -import type { HostChildren } from 'ReactFiberReconciler'; var ReactFiberReconciler = require('ReactFiberReconciler'); var ReactInstanceMap = require('ReactInstanceMap'); @@ -32,59 +31,35 @@ var { var scheduledAnimationCallback = null; var scheduledDeferredCallback = null; -const TERMINAL_TAG = 99; -const TEXT_TAG = 98; - type Container = { rootID: string, children: Array }; type Props = { prop: any }; -type Instance = { tag: 99, type: string, id: number, children: Array, prop: any }; -type TextInstance = { tag: 98, text: string }; +type Instance = {| type: string, id: number, children: Array, prop: any |}; +type TextInstance = {| text: string, id: number |}; var instanceCounter = 0; -function recursivelyAppendChildren( - flatArray : Array, - child : HostChildren -) { - if (!child) { - return; - } - if (child.tag === TERMINAL_TAG || child.tag === TEXT_TAG) { - flatArray.push(child); - } else { - let node = child; - do { - recursivelyAppendChildren(flatArray, node.output); - } while (node = node.sibling); - } -} - -function flattenChildren(children : HostChildren) { - const flatArray = []; - recursivelyAppendChildren(flatArray, children); - return flatArray; -} - var NoopRenderer = ReactFiberReconciler({ - updateContainer(containerInfo : Container, children : HostChildren) : void { - containerInfo.children = flattenChildren(children); - }, - - createInstance(type : string, props : Props, children : HostChildren) : Instance { + createInstance(type : string, props : Props) : Instance { const inst = { - tag: TERMINAL_TAG, id: instanceCounter++, type: type, - children: flattenChildren(children), + children: [], prop: props.prop, }; // Hide from unit tests - Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false }); Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false }); return inst; }, + appendInitialChild(parentInstance : Instance, child : Instance | TextInstance) : void { + parentInstance.children.push(child); + }, + + finalizeInitialChildren(domElement : Instance, type : string, props : Props) : void { + // Noop + }, + prepareUpdate(instance : Instance, oldProps : Props, newProps : Props) : boolean { return true; }, @@ -94,9 +69,9 @@ var NoopRenderer = ReactFiberReconciler({ }, createTextInstance(text : string) : TextInstance { - var inst = { tag: TEXT_TAG, text : text }; + var inst = { text : text, id: instanceCounter++ }; // Hide from unit tests - Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false }); + Object.defineProperty(inst, 'id', { value: inst.id, enumerable: false }); return inst; }, @@ -104,7 +79,7 @@ var NoopRenderer = ReactFiberReconciler({ textInstance.text = newText; }, - appendChild(parentInstance : Instance, child : Instance | TextInstance) : void { + appendChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void { const index = parentInstance.children.indexOf(child); if (index !== -1) { parentInstance.children.splice(index, 1); @@ -113,7 +88,7 @@ var NoopRenderer = ReactFiberReconciler({ }, insertBefore( - parentInstance : Instance, + parentInstance : Instance | Container, child : Instance | TextInstance, beforeChild : Instance | TextInstance ) : void { @@ -128,7 +103,7 @@ var NoopRenderer = ReactFiberReconciler({ parentInstance.children.splice(beforeIndex, 0, child); }, - removeChild(parentInstance : Instance, child : Instance | TextInstance) : void { + removeChild(parentInstance : Instance | Container, child : Instance | TextInstance) : void { const index = parentInstance.children.indexOf(child); if (index === -1) { throw new Error('This child does not exist.'); @@ -213,7 +188,7 @@ var ReactNoop = { } // Unsound duck typing. const component = (componentOrElement : any); - if (component.tag === TERMINAL_TAG || component.tag === TEXT_TAG) { + if (typeof component.id === 'number') { return component; } const inst = ReactInstanceMap.get(component); @@ -278,10 +253,13 @@ var ReactNoop = { function logHostInstances(children: Array, depth) { for (var i = 0; i < children.length; i++) { var child = children[i]; - if (child.tag === TEXT_TAG) { - log(' '.repeat(depth) + '- ' + child.text); + var indent = ' '.repeat(depth); + if (typeof child.text === 'string') { + log(indent + '- ' + child.text); } else { - log(' '.repeat(depth) + '- ' + child.type + '#' + child.id); + // $FlowFixMe - The child should've been refined now. + log(indent + '- ' + child.type + '#' + child.id); + // $FlowFixMe - The child should've been refined now. logHostInstances(child.children, depth + 1); } } diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 5d669db55251..887d142691ce 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -300,7 +300,24 @@ module.exports = function( } function updatePortalComponent(current, workInProgress) { - reconcileChildren(current, workInProgress, workInProgress.pendingProps); + const priorityLevel = workInProgress.pendingWorkPriority; + const nextChildren = workInProgress.pendingProps; + if (!current) { + // Portals are special because we don't append the children during mount + // but at commit. Therefore we need to track insertions which the normal + // flow doesn't do during mount. This doesn't happen at the root because + // the root always starts with a "current" with a null child. + // TODO: Consider unifying this with how the root works. + workInProgress.child = reconcileChildFibersInPlace( + workInProgress, + workInProgress.child, + nextChildren, + priorityLevel + ); + markChildAsProgressed(current, workInProgress, priorityLevel); + } else { + reconcileChildren(current, workInProgress, nextChildren); + } } /* diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 3b195734df0b..de09d2af44d2 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -13,7 +13,6 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; -import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; var ReactTypeOfWork = require('ReactTypeOfWork'); @@ -37,7 +36,6 @@ module.exports = function( trapError : (failedFiber : Fiber, error: Error, isUnmounting : boolean) => void ) { - const updateContainer = config.updateContainer; const commitUpdate = config.commitUpdate; const commitTextUpdate = config.commitTextUpdate; @@ -68,20 +66,28 @@ module.exports = function( } } - function getHostParent(fiber : Fiber) : ?I { + function getHostParent(fiber : Fiber) : I | C { let parent = fiber.return; while (parent) { switch (parent.tag) { case HostComponent: return parent.stateNode; case HostContainer: - // TODO: Currently we use the updateContainer feature to update these, - // but we should be able to handle this case too. - return null; + return parent.stateNode.containerInfo; + case Portal: + return parent.stateNode.containerInfo; } parent = parent.return; } - return null; + throw new Error('Expected to find a host parent.'); + } + + function isHostParent(fiber : Fiber) : boolean { + return ( + fiber.tag === HostComponent || + fiber.tag === HostContainer || + fiber.tag === Portal + ); } function getHostSibling(fiber : Fiber) : ?I { @@ -93,7 +99,7 @@ module.exports = function( siblings: while (true) { // If we didn't find anything, let's try the next sibling. while (!node.sibling) { - if (!node.return || node.return.tag === HostComponent) { + if (!node.return || isHostParent(node.return)) { // If we pop out of the root or hit the parent the fiber we are the // last sibling. return null; @@ -128,9 +134,6 @@ module.exports = function( function commitInsertion(finishedWork : Fiber) : void { // Recursively insert all host nodes into the parent. const parent = getHostParent(finishedWork); - if (!parent) { - return; - } const before = getHostSibling(finishedWork); // We only have the top Fiber that was inserted but we need recurse down its // children to find all the terminal nodes. @@ -142,6 +145,10 @@ module.exports = function( } else { appendChild(parent, node.stateNode); } + } else if (node.tag === Portal) { + // If the insertion itself is a portal, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. } else if (node.child) { // TODO: Coroutines need to visit the stateNode. node = node.child; @@ -198,8 +205,14 @@ module.exports = function( commitNestedUnmounts(node); // After all the children have unmounted, it is now safe to remove the // node from the tree. - if (parent) { - removeChild(parent, node.stateNode); + removeChild(parent, node.stateNode); + } else if (node.tag === Portal) { + // When we go into a portal, it becomes the parent to remove from. + // We will reassign it back when we pop the portal on the way up. + parent = node.stateNode.containerInfo; + if (node.child) { + node = node.child; + continue; } } else { commitUnmount(node); @@ -218,6 +231,11 @@ module.exports = function( return; } node = node.return; + if (node.tag === Portal) { + // When we go out of the portal, we need to restore the parent. + // Since we don't keep a stack of them, we will search for it. + parent = getHostParent(node); + } } node.sibling.return = node.return; node = node.sibling; @@ -260,11 +278,6 @@ module.exports = function( detachRef(current); return; } - case Portal: { - const containerInfo : C = current.stateNode.containerInfo; - updateContainer(containerInfo, null); - return; - } } } @@ -274,14 +287,6 @@ module.exports = function( detachRefIfNeeded(current, finishedWork); return; } - case HostContainer: { - // TODO: Attach children to root container. - const children = finishedWork.output; - const root : FiberRoot = finishedWork.stateNode; - const containerInfo : C = root.containerInfo; - updateContainer(containerInfo, children); - return; - } case HostComponent: { const instance : I = finishedWork.stateNode; if (instance != null && current) { @@ -303,10 +308,10 @@ module.exports = function( commitTextUpdate(textInstance, oldText, newText); return; } + case HostContainer: { + return; + } case Portal: { - const children = finishedWork.child; - const containerInfo : C = finishedWork.stateNode.containerInfo; - updateContainer(containerInfo, children); return; } default: diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index dc4ab6844331..d62a2926b762 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -46,6 +46,8 @@ var { module.exports = function(config : HostConfig) { const createInstance = config.createInstance; + const appendInitialChild = config.appendInitialChild; + const finalizeInitialChildren = config.finalizeInitialChildren; const createTextInstance = config.createTextInstance; const prepareUpdate = config.prepareUpdate; @@ -124,6 +126,35 @@ module.exports = function(config : HostConfig) { return workInProgress.stateNode; } + function appendAllChildren(parent : I, workInProgress : Fiber) { + // We only have the top Fiber that was created but we need recurse down its + // children to find all the terminal nodes. + let node = workInProgress.child; + while (node) { + if (node.tag === HostComponent || node.tag === HostText) { + appendInitialChild(parent, node.stateNode); + } else if (node.tag === Portal) { + // If we have a portal child, then we don't want to traverse + // down its children. Instead, we'll get insertions from each child in + // the portal directly. + } else if (node.child) { + // TODO: Coroutines need to visit the stateNode. + node = node.child; + continue; + } + if (node === workInProgress) { + return; + } + while (!node.sibling) { + if (!node.return || node.return === workInProgress) { + return; + } + node = node.return; + } + node = node.sibling; + } + } + function completeWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber { switch (workInProgress.tag) { case FunctionalComponent: @@ -167,11 +198,8 @@ module.exports = function(config : HostConfig) { fiberRoot.context = fiberRoot.pendingContext; fiberRoot.pendingContext = null; } - // We don't know if a container has updated any children so we always - // need to update it right now. We schedule this side-effect before - // all the other side-effects in the subtree. We need to schedule it - // before so that the entire tree is up-to-date before the life-cycles - // are invoked. + // TODO: Only mark this as an update if we have any pending callbacks + // on it. markUpdate(workInProgress); return null; } @@ -204,9 +232,16 @@ module.exports = function(config : HostConfig) { return null; } } - const child = workInProgress.child; - const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child; - const instance = createInstance(workInProgress.type, newProps, children, workInProgress); + + // TODO: Move createInstance to beginWork and keep it on a context + // "stack" as the parent. Then append children as we go in beginWork + // or completeWork depending on we want to add then top->down or + // bottom->up. Top->down is faster in IE11. + // Finally, finalizeInitialChildren here in completeWork. + const instance = createInstance(workInProgress.type, newProps, workInProgress); + appendAllChildren(instance, workInProgress); + finalizeInitialChildren(instance, workInProgress.type, newProps); + // TODO: This seems like unnecessary duplication. workInProgress.stateNode = instance; workInProgress.output = instance; @@ -253,6 +288,7 @@ module.exports = function(config : HostConfig) { transferOutput(workInProgress.child, workInProgress); return null; case Portal: + // TODO: Only mark this as an update if we have any pending callbacks. markUpdate(workInProgress); workInProgress.output = null; workInProgress.memoizedProps = workInProgress.pendingProps; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 15b415f88d7a..a6b54e85a93b 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -47,22 +47,19 @@ type OpaqueNode = Fiber; export type HostConfig = { - // TODO: We don't currently have a quick way to detect that children didn't - // reorder so we host will always need to check the set. We should make a flag - // or something so that it can bailout easily. + createInstance(type : T, props : P, internalInstanceHandle : OpaqueNode) : I, + appendInitialChild(parentInstance : I, child : I) : void, + finalizeInitialChildren(parentInstance : I, type : T, props : P) : void, - updateContainer(containerInfo : C, children : HostChildren) : void, - - createInstance(type : T, props : P, children : HostChildren, internalInstanceHandle : OpaqueNode) : I, prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean, commitUpdate(instance : I, oldProps : P, newProps : P, internalInstanceHandle : OpaqueNode) : void, createTextInstance(text : string, internalInstanceHandle : OpaqueNode) : TI, commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void, - appendChild(parentInstance : I, child : I | TI) : void, - insertBefore(parentInstance : I, child : I | TI, beforeChild : I | TI) : void, - removeChild(parentInstance : I, child : I | TI) : void, + appendChild(parentInstance : I | C, child : I | TI) : void, + insertBefore(parentInstance : I | C, child : I | TI, beforeChild : I | TI) : void, + removeChild(parentInstance : I | C, child : I | TI) : void, scheduleAnimationCallback(callback : () => void) : void, scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void,