} */
+ let promise = new Promise((resolve, reject) => {
+ resolver = result => {
+ data = result;
+ resolve(result);
+ return promise;
+ };
+
+ rejecter = e => {
+ error = e;
+ reject(e);
+ return promise;
+ };
+ });
+
+ function useSuspenseLoader() {
+ if (error) {
+ throw error;
+ }
+
+ if (!data) {
+ throw promise;
+ }
+
+ return data;
+ }
+
+ return [useSuspenseLoader, resolver, rejecter];
+}
diff --git a/jsx-runtime/src/index.js b/jsx-runtime/src/index.js
index e189ca0b02..8cbe9ea444 100644
--- a/jsx-runtime/src/index.js
+++ b/jsx-runtime/src/index.js
@@ -2,8 +2,6 @@ import { options, Fragment } from 'preact';
import { encodeEntities } from './utils';
import { IS_NON_DIMENSIONAL } from '../../src/constants';
-/** @typedef {import('preact').VNode} VNode */
-
let vnodeId = 0;
const isArray = Array.isArray;
@@ -43,6 +41,7 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) {
}
}
+ /** @type {VNode & { __source: any; __self: any }} */
const vnode = {
type,
props: normalizedProps,
@@ -54,10 +53,10 @@ function createVNode(type, props, key, isStaticChildren, __source, __self) {
_dom: null,
_nextDom: undefined,
_component: null,
- _hydrating: null,
constructor: undefined,
_original: --vnodeId,
_index: -1,
+ _flags: 0,
__source,
__self
};
diff --git a/mangle.json b/mangle.json
index 76e9fb5956..7744675ce2 100644
--- a/mangle.json
+++ b/mangle.json
@@ -52,9 +52,9 @@
"$_onResolve": "__R",
"$_suspended": "__a",
"$_dom": "__e",
- "$_hydrating": "__h",
"$_component": "__c",
"$_index": "__i",
+ "$_flags": "__u",
"$__html": "__html",
"$_parent": "__",
"$_pendingError": "__E",
diff --git a/src/component.js b/src/component.js
index 73c5afd2e4..d1e0d27579 100644
--- a/src/component.js
+++ b/src/component.js
@@ -2,6 +2,7 @@ import { assign } from './util';
import { diff, commitRoot } from './diff/index';
import options from './options';
import { Fragment } from './create-element';
+import { MODE_HYDRATE } from './constants';
/**
* Base Component class. Provides `setState()` and `forceUpdate()`, which
@@ -122,12 +123,11 @@ export function getDomSibling(vnode, childIndex) {
function renderComponent(component) {
let oldVNode = component._vnode,
oldDom = oldVNode._dom,
- parentDom = component._parentDom;
+ parentDom = component._parentDom,
+ commitQueue = [],
+ refQueue = [];
if (parentDom) {
- let commitQueue = [],
- refQueue = [];
-
const newVNode = assign({}, oldVNode);
newVNode._original = oldVNode._original + 1;
@@ -137,10 +137,10 @@ function renderComponent(component) {
oldVNode,
component._globalContext,
parentDom.ownerSVGElement !== undefined,
- oldVNode._hydrating != null ? [oldDom] : null,
+ oldVNode._flags & MODE_HYDRATE ? [oldDom] : null,
commitQueue,
oldDom == null ? getDomSibling(oldVNode) : oldDom,
- oldVNode._hydrating,
+ !!(oldVNode._flags & MODE_HYDRATE),
refQueue
);
diff --git a/src/constants.js b/src/constants.js
index a8fc694027..3bcec6cfac 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -1,3 +1,15 @@
+/** Normal hydration that attaches to a DOM tree but does not diff it. */
+export const MODE_HYDRATE = 1 << 5;
+/** Signifies this VNode suspended on the previous render */
+export const MODE_SUSPENDED = 1 << 7;
+/** Indicates that this node needs to be inserted while patching children */
+export const INSERT_VNODE = 1 << 16;
+/** Indicates a VNode has been matched with another VNode in the diff */
+export const MATCHED = 1 << 17;
+
+/** Reset all mode flags */
+export const RESET_MODE = ~(MODE_HYDRATE | MODE_SUSPENDED);
+
export const EMPTY_OBJ = /** @type {any} */ ({});
export const EMPTY_ARR = [];
export const IS_NON_DIMENSIONAL =
diff --git a/src/create-element.js b/src/create-element.js
index 239b7efdd7..66898b2224 100644
--- a/src/create-element.js
+++ b/src/create-element.js
@@ -72,10 +72,10 @@ export function createVNode(type, props, key, ref, original) {
// a _nextDom that has been set to `null`
_nextDom: undefined,
_component: null,
- _hydrating: null,
constructor: undefined,
_original: original == null ? ++vnodeId : original,
- _index: -1
+ _index: -1,
+ _flags: 0
};
// Only invoke the vnode hook if this was *not* a direct copy:
diff --git a/src/diff/children.js b/src/diff/children.js
index 251c6b63d2..0d3b704b51 100644
--- a/src/diff/children.js
+++ b/src/diff/children.js
@@ -1,6 +1,6 @@
import { diff, unmount, applyRef } from './index';
import { createVNode, Fragment } from '../create-element';
-import { EMPTY_OBJ, EMPTY_ARR } from '../constants';
+import { EMPTY_OBJ, EMPTY_ARR, INSERT_VNODE, MATCHED } from '../constants';
import { isArray } from '../util';
import { getDomSibling } from '../component';
@@ -40,7 +40,6 @@ export function diffChildren(
refQueue
) {
let i,
- j,
/** @type {VNode} */
oldVNode,
/** @type {VNode} */
@@ -48,17 +47,132 @@ export function diffChildren(
/** @type {PreactElement} */
newDom,
/** @type {PreactElement} */
- firstChildDom,
- skew = 0;
+ firstChildDom;
// This is a compression of oldParentVNode!=null && oldParentVNode != EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR
// as EMPTY_OBJ._children should be `undefined`.
/** @type {VNode[]} */
let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
+ let newChildrenLength = renderResult.length;
+
+ newParentVNode._nextDom = oldDom;
+ constructNewChildrenArray(newParentVNode, renderResult, oldChildren);
+ oldDom = newParentVNode._nextDom;
+
+ for (i = 0; i < newChildrenLength; i++) {
+ childVNode = newParentVNode._children[i];
+
+ if (
+ childVNode == null ||
+ typeof childVNode == 'boolean' ||
+ typeof childVNode == 'function'
+ ) {
+ continue;
+ }
+
+ // At this point, constructNewChildrenArray has assigned _index to be the
+ // matchingIndex for this VNode's oldVNode (or -1 if there is no oldVNode).
+ if (childVNode._index === -1) {
+ oldVNode = EMPTY_OBJ;
+ } else {
+ oldVNode = oldChildren[childVNode._index] || EMPTY_OBJ;
+ }
+
+ // Update childVNode._index to its final index
+ childVNode._index = i;
+
+ // Morph the old element into the new one, but don't append it to the dom yet
+ diff(
+ parentDom,
+ childVNode,
+ oldVNode,
+ globalContext,
+ isSvg,
+ excessDomChildren,
+ commitQueue,
+ oldDom,
+ isHydrating,
+ refQueue
+ );
+
+ // Adjust DOM nodes
+ newDom = childVNode._dom;
+ if (childVNode.ref && oldVNode.ref != childVNode.ref) {
+ if (oldVNode.ref) {
+ applyRef(oldVNode.ref, null, childVNode);
+ }
+ refQueue.push(
+ childVNode.ref,
+ childVNode._component || newDom,
+ childVNode
+ );
+ }
+
+ if (firstChildDom == null && newDom != null) {
+ firstChildDom = newDom;
+ }
+
+ if (
+ childVNode._flags & INSERT_VNODE ||
+ oldVNode._children === childVNode._children
+ ) {
+ oldDom = insert(childVNode, oldDom, parentDom);
+ } else if (
+ typeof childVNode.type == 'function' &&
+ childVNode._nextDom !== undefined
+ ) {
+ // Since Fragments or components that return Fragment like VNodes can
+ // contain multiple DOM nodes as the same level, continue the diff from
+ // the sibling of last DOM child of this child VNode
+ oldDom = childVNode._nextDom;
+ } else if (newDom) {
+ oldDom = newDom.nextSibling;
+ }
+
+ // Eagerly cleanup _nextDom. We don't need to persist the value because it
+ // is only used by `diffChildren` to determine where to resume the diff
+ // after diffing Components and Fragments. Once we store it the nextDOM
+ // local var, we can clean up the property. Also prevents us hanging on to
+ // DOM nodes that may have been unmounted.
+ childVNode._nextDom = undefined;
+
+ // Unset diffing flags
+ childVNode._flags &= ~(INSERT_VNODE | MATCHED);
+ }
+
+ // TODO: With new child diffing algo, consider alt ways to diff Fragments.
+ // Such as dropping oldDom and moving fragments in place
+ //
+ // Because the newParentVNode is Fragment-like, we need to set it's
+ // _nextDom property to the nextSibling of its last child DOM node.
+ //
+ // `oldDom` contains the correct value here because if the last child
+ // is a Fragment-like, then oldDom has already been set to that child's _nextDom.
+ // If the last child is a DOM VNode, then oldDom will be set to that DOM
+ // node's nextSibling.
+ newParentVNode._nextDom = oldDom;
+ newParentVNode._dom = firstChildDom;
+}
+
+/**
+ * @param {VNode} newParentVNode
+ * @param {ComponentChildren[]} renderResult
+ * @param {VNode[]} oldChildren
+ */
+function constructNewChildrenArray(newParentVNode, renderResult, oldChildren) {
+ /** @type {number} */
+ let i;
+ /** @type {VNode} */
+ let childVNode;
+ /** @type {VNode} */
+ let oldVNode;
+
+ const newChildrenLength = renderResult.length;
let oldChildrenLength = oldChildren.length,
- remainingOldChildren = oldChildrenLength,
- newChildrenLength = renderResult.length;
+ remainingOldChildren = oldChildrenLength;
+
+ let skew = 0;
newParentVNode._children = [];
for (i = 0; i < newChildrenLength; i++) {
@@ -113,24 +227,27 @@ export function diffChildren(
childVNode = newParentVNode._children[i] = childVNode;
}
- // Terser removes the `continue` here and wraps the loop body
- // in a `if (childVNode) { ... } condition
+ // Handle unmounting null placeholders, i.e. VNode => null in unkeyed children
if (childVNode == null) {
oldVNode = oldChildren[i];
if (oldVNode && oldVNode.key == null && oldVNode._dom) {
- if (oldVNode._dom == oldDom) {
- oldDom = getDomSibling(oldVNode);
-
- if (typeof newParentVNode.type == 'function') {
- // If the parent VNode is a component/fragment, make sure its diff
- // continues with a DOM node that is still mounted in case this loop
- // exits here because the rest of the new children are `null`.
- newParentVNode._nextDom = oldDom;
- }
+ if (oldVNode._dom == newParentVNode._nextDom) {
+ newParentVNode._nextDom = getDomSibling(oldVNode);
}
unmount(oldVNode, oldVNode, false);
+
+ // Explicitly nullify this position in oldChildren instead of just
+ // setting `_match=true` to prevent other routines (e.g.
+ // `findMatchingIndex` or `getDomSibling`) from thinking VNodes or DOM
+ // nodes in this position are still available to be used in diffing when
+ // they have actually already been unmounted. For example, by only
+ // setting `_match=true` here, the unmounting loop later would attempt
+ // to unmount this VNode again seeing `_match==true`. Further,
+ // getDomSibling doesn't know about _match and so would incorrectly
+ // assume DOM nodes in this subtree are mounted and usable.
oldChildren[i] = null;
+ remainingOldChildren--;
}
continue;
@@ -138,9 +255,8 @@ export function diffChildren(
childVNode._parent = newParentVNode;
childVNode._depth = newParentVNode._depth + 1;
- childVNode._index = i;
- let skewedIndex = i + skew;
+ const skewedIndex = i + skew;
const matchingIndex = findMatchingIndex(
childVNode,
oldChildren,
@@ -148,41 +264,26 @@ export function diffChildren(
remainingOldChildren
);
- if (matchingIndex === -1) {
- oldVNode = EMPTY_OBJ;
- } else {
- oldVNode = oldChildren[matchingIndex] || EMPTY_OBJ;
- oldChildren[matchingIndex] = undefined;
- remainingOldChildren--;
- }
-
- // Morph the old element into the new one, but don't append it to the dom yet
- diff(
- parentDom,
- childVNode,
- oldVNode,
- globalContext,
- isSvg,
- excessDomChildren,
- commitQueue,
- oldDom,
- isHydrating,
- refQueue
- );
+ // Temporarily store the matchingIndex on the _index property so we can pull
+ // out the oldVNode in diffChildren. We'll override this to the VNode's
+ // final index after using this property to get the oldVNode
+ childVNode._index = matchingIndex;
- newDom = childVNode._dom;
- if ((j = childVNode.ref) && oldVNode.ref != j) {
- if (oldVNode.ref) {
- applyRef(oldVNode.ref, null, childVNode);
+ if (matchingIndex !== -1) {
+ remainingOldChildren--;
+ if (oldChildren[matchingIndex]) {
+ oldChildren[matchingIndex]._flags |= MATCHED;
}
- refQueue.push(j, childVNode._component || newDom, childVNode);
}
- if (firstChildDom == null && newDom != null) {
- firstChildDom = newDom;
- }
+ // Here, we define isMounting for the purposes of the skew diffing
+ // algorithm. Nodes that are unsuspending are considered mounting and we detect
+ // this by checking if oldVNode._original === null
+ const isMounting =
+ matchingIndex === -1 ||
+ oldChildren[matchingIndex] == null ||
+ oldChildren[matchingIndex]._original === null;
- let isMounting = oldVNode === EMPTY_OBJ || oldVNode._original === null;
if (isMounting) {
if (matchingIndex == -1) {
skew--;
@@ -208,98 +309,61 @@ export function diffChildren(
}
}
- skewedIndex = i + skew;
-
- if (typeof childVNode.type == 'function') {
- if (
- matchingIndex !== skewedIndex ||
- oldVNode._children === childVNode._children
- ) {
- oldDom = reorderChildren(childVNode, oldDom, parentDom);
- } else if (childVNode._nextDom !== undefined) {
- // Only Fragments or components that return Fragment like VNodes will
- // have a non-undefined _nextDom. Continue the diff from the sibling
- // of last DOM child of this child VNode
- oldDom = childVNode._nextDom;
- } else if (newDom) {
- oldDom = newDom.nextSibling;
- }
-
- // Eagerly cleanup _nextDom. We don't need to persist the value because
- // it is only used by `diffChildren` to determine where to resume the diff after
- // diffing Components and Fragments. Once we store it the nextDOM local var, we
- // can clean up the property
- childVNode._nextDom = undefined;
- } else if (newDom) {
- if (matchingIndex !== skewedIndex || isMounting) {
- oldDom = placeChild(parentDom, newDom, oldDom);
- } else {
- oldDom = newDom.nextSibling;
- }
- }
-
- if (typeof newParentVNode.type == 'function') {
- // Because the newParentVNode is Fragment-like, we need to set it's
- // _nextDom property to the nextSibling of its last child DOM node.
- //
- // `oldDom` contains the correct value here because if the last child
- // is a Fragment-like, then oldDom has already been set to that child's _nextDom.
- // If the last child is a DOM VNode, then oldDom will be set to that DOM
- // node's nextSibling.
- newParentVNode._nextDom = oldDom;
+ // Move this VNode's DOM if the original index (matchingIndex) doesn't match
+ // the new skew index (i + new skew) or it's a mounting DOM VNode
+ if (
+ matchingIndex !== i + skew ||
+ (typeof childVNode.type != 'function' && isMounting)
+ ) {
+ childVNode._flags |= INSERT_VNODE;
}
}
- newParentVNode._dom = firstChildDom;
-
- // Remove remaining oldChildren if there are any.
- for (i = oldChildrenLength; i--; ) {
- if (oldChildren[i] != null) {
- if (
- typeof newParentVNode.type == 'function' &&
- oldChildren[i]._dom != null &&
- oldChildren[i]._dom == oldDom
- ) {
- // If oldDom points to a dom node that is about to be unmounted, then
- // get the next sibling of that vnode and set _nextDom to it, so the
- // parent's diff continues diffing an existing DOM node
- newParentVNode._nextDom = oldChildren[i]._dom.nextSibling;
+ // Remove remaining oldChildren if there are any. Loop forwards so that as we
+ // unmount DOM from the beginning of the oldChildren, we can adjust oldDom to
+ // point to the next child, which needs to be the first DOM node that won't be
+ // unmounted.
+ for (i = 0; i < oldChildrenLength; i++) {
+ oldVNode = oldChildren[i];
+ if (oldVNode != null && (oldVNode._flags & MATCHED) === 0) {
+ if (oldVNode._dom == newParentVNode._nextDom) {
+ newParentVNode._nextDom = getDomSibling(oldVNode);
}
- unmount(oldChildren[i], oldChildren[i]);
+ unmount(oldVNode, oldVNode);
}
}
}
/**
- * @param {VNode} childVNode
+ * @param {VNode} parentVNode
* @param {PreactElement} oldDom
* @param {PreactElement} parentDom
* @returns {PreactElement}
*/
-function reorderChildren(childVNode, oldDom, parentDom) {
+function insert(parentVNode, oldDom, parentDom) {
// Note: VNodes in nested suspended trees may be missing _children.
- let c = childVNode._children;
-
- let tmp = 0;
- for (; c && tmp < c.length; tmp++) {
- let vnode = c[tmp];
- if (vnode) {
- // We typically enter this code path on sCU bailout, where we copy
- // oldVNode._children to newVNode._children. If that is the case, we need
- // to update the old children's _parent pointer to point to the newVNode
- // (childVNode here).
- vnode._parent = childVNode;
-
- if (typeof vnode.type == 'function') {
- oldDom = reorderChildren(vnode, oldDom, parentDom);
- } else {
- oldDom = placeChild(parentDom, vnode._dom, oldDom);
+
+ if (typeof parentVNode.type == 'function') {
+ let children = parentVNode._children;
+ for (let i = 0; children && i < children.length; i++) {
+ if (children[i]) {
+ // If we enter this code path on sCU bailout, where we copy
+ // oldVNode._children to newVNode._children, we need to update the old
+ // children's _parent pointer to point to the newVNode (parentVNode
+ // here).
+ children[i]._parent = parentVNode;
+ oldDom = insert(children[i], oldDom, parentDom);
}
}
+
+ return oldDom;
+ } else if (parentVNode._dom != oldDom) {
+ parentDom.insertBefore(parentVNode._dom, oldDom || null);
+ oldDom = parentVNode._dom;
}
- return oldDom;
+ return oldDom && oldDom.nextSibling;
}
/**
@@ -321,20 +385,6 @@ export function toChildArray(children, out) {
return out;
}
-/**
- * @param {PreactElement} parentDom
- * @param {PreactElement} newDom
- * @param {PreactElement} oldDom
- * @returns {PreactElement}
- */
-function placeChild(parentDom, newDom, oldDom) {
- if (newDom != oldDom) {
- parentDom.insertBefore(newDom, oldDom || null);
- }
-
- return newDom.nextSibling;
-}
-
/**
* @param {VNode} childVNode
* @param {VNode[]} oldChildren
@@ -354,16 +404,33 @@ function findMatchingIndex(
let y = skewedIndex + 1;
let oldVNode = oldChildren[skewedIndex];
+ // We only need to perform a search if there are more children
+ // (remainingOldChildren) to search. However, if the oldVNode we just looked
+ // at skewedIndex was not already used in this diff, then there must be at
+ // least 1 other (so greater than 1) remainingOldChildren to attempt to match
+ // against. So the following condition checks that ensuring
+ // remainingOldChildren > 1 if the oldVNode is not already used/matched. Else
+ // if the oldVNode was null or matched, then there could needs to be at least
+ // 1 (aka `remainingOldChildren > 0`) children to find and compare against.
+ let shouldSearch =
+ remainingOldChildren >
+ (oldVNode != null && (oldVNode._flags & MATCHED) === 0 ? 1 : 0);
+
if (
oldVNode === null ||
(oldVNode && key == oldVNode.key && type === oldVNode.type)
) {
return skewedIndex;
- } else if (remainingOldChildren > (oldVNode != null ? 1 : 0)) {
+ } else if (shouldSearch) {
while (x >= 0 || y < oldChildren.length) {
if (x >= 0) {
oldVNode = oldChildren[x];
- if (oldVNode && key == oldVNode.key && type === oldVNode.type) {
+ if (
+ oldVNode &&
+ (oldVNode._flags & MATCHED) === 0 &&
+ key == oldVNode.key &&
+ type === oldVNode.type
+ ) {
return x;
}
x--;
@@ -371,7 +438,12 @@ function findMatchingIndex(
if (y < oldChildren.length) {
oldVNode = oldChildren[y];
- if (oldVNode && key == oldVNode.key && type === oldVNode.type) {
+ if (
+ oldVNode &&
+ (oldVNode._flags & MATCHED) === 0 &&
+ key == oldVNode.key &&
+ type === oldVNode.type
+ ) {
return y;
}
y++;
diff --git a/src/diff/index.js b/src/diff/index.js
index 17254a32f6..515f1b40ab 100644
--- a/src/diff/index.js
+++ b/src/diff/index.js
@@ -1,4 +1,9 @@
-import { EMPTY_OBJ } from '../constants';
+import {
+ EMPTY_OBJ,
+ MODE_HYDRATE,
+ MODE_SUSPENDED,
+ RESET_MODE
+} from '../constants';
import { BaseComponent, getDomSibling } from '../component';
import { Fragment } from '../create-element';
import { diffChildren } from './children';
@@ -45,11 +50,9 @@ export function diff(
if (newVNode.constructor !== undefined) return null;
// If the previous diff bailed out, resume creating/hydrating.
- if (oldVNode._hydrating != null) {
- isHydrating = oldVNode._hydrating;
+ if (oldVNode._flags & MODE_SUSPENDED) {
+ isHydrating = !!(oldVNode._flags & MODE_HYDRATE);
oldDom = newVNode._dom = oldVNode._dom;
- // if we resume, we want the tree to be "unlocked"
- newVNode._hydrating = null;
excessDomChildren = [oldDom];
}
@@ -253,7 +256,7 @@ export function diff(
c.base = newVNode._dom;
// We successfully rendered this VNode, unset any stored hydration/bailout state:
- newVNode._hydrating = null;
+ newVNode._flags &= RESET_MODE;
if (c._renderCallbacks.length) {
commitQueue.push(c);
@@ -267,7 +270,9 @@ export function diff(
// if hydrating or creating initial tree, bailout preserves DOM:
if (isHydrating || excessDomChildren != null) {
newVNode._dom = oldDom;
- newVNode._hydrating = !!isHydrating;
+ newVNode._flags |= isHydrating
+ ? MODE_HYDRATE | MODE_SUSPENDED
+ : MODE_HYDRATE;
excessDomChildren[excessDomChildren.indexOf(oldDom)] = null;
// ^ could possibly be simplified to:
// excessDomChildren.length = 0;
diff --git a/src/internal.d.ts b/src/internal.d.ts
index ea56157b1e..fda7ce89fd 100644
--- a/src/internal.d.ts
+++ b/src/internal.d.ts
@@ -147,10 +147,10 @@ declare global {
*/
_nextDom: PreactElement | null | undefined;
_component: Component | null;
- _hydrating: boolean | null;
constructor: undefined;
_original: number;
_index: number;
+ _flags: number;
}
export interface Component extends preact.Component
{
diff --git a/test/browser/createContext.test.js b/test/browser/createContext.test.js
index f4610ed372..b27f20b625 100644
--- a/test/browser/createContext.test.js
+++ b/test/browser/createContext.test.js
@@ -877,8 +877,8 @@ describe('createContext', () => {
expect(events).to.deep.equal([
'render 0',
'mount 0',
- 'render 1',
'unmount 0',
+ 'render 1',
'mount 1'
]);
});
diff --git a/test/browser/fragments.test.js b/test/browser/fragments.test.js
index 3e5292f2c5..20fed865e4 100644
--- a/test/browser/fragments.test.js
+++ b/test/browser/fragments.test.js
@@ -1,7 +1,7 @@
import { setupRerender } from 'preact/test-utils';
import { createElement, render, Component, Fragment } from 'preact';
import { setupScratch, teardown } from '../_util/helpers';
-import { span, div, ul, ol, li, section, p } from '../_util/dom';
+import { span, div, ul, ol, li, section } from '../_util/dom';
import { logCall, clearLog, getLog } from '../_util/logCall';
/** @jsx createElement */
@@ -235,9 +235,9 @@ describe('Fragment', () => {
expect(scratch.innerHTML).to.equal(div([div(1), span(2), span(2)]));
expectDomLogToBe([
+ '1.remove()',
'.appendChild(#text)',
- '
122.insertBefore(
1,
1)',
- '1.remove()'
+ '22.insertBefore(
1,
2)'
]);
});
@@ -357,9 +357,9 @@ describe('Fragment', () => {
expect(ops).to.deep.equal([]);
expect(scratch.innerHTML).to.equal('Hello
');
expectDomLogToBe([
+ 'Hello.remove()',
'
.appendChild(#text)',
- '
Hello.insertBefore(
Hello,
Hello)',
- '
Hello.remove()'
+ '
.appendChild(
Hello)'
]);
clearLog();
@@ -368,10 +368,10 @@ describe('Fragment', () => {
expect(ops).to.deep.equal([]);
expect(scratch.innerHTML).to.equal('
Hello
');
expectDomLogToBe([
+ '
Hello.remove()',
'
.appendChild(#text)',
// Re-append the Stateful DOM since it has been re-parented
- '
Hello.insertBefore(
Hello,
Hello)',
- '
Hello.remove()'
+ '
.appendChild(
Hello)'
]);
});
@@ -396,9 +396,9 @@ describe('Fragment', () => {
expect(ops).to.deep.equal([]);
expect(scratch.innerHTML).to.equal('
Hello
');
expectDomLogToBe([
+ '
Hello.remove()',
'
.appendChild(#text)',
- '
Hello.insertBefore(
Hello,
Hello)',
- '
Hello.remove()'
+ '
.appendChild(
Hello)'
]);
clearLog();
@@ -407,9 +407,9 @@ describe('Fragment', () => {
expect(ops).to.deep.equal([]);
expect(scratch.innerHTML).to.equal('
Hello
');
expectDomLogToBe([
+ '
Hello.remove()',
'
.appendChild(#text)',
- '
Hello.insertBefore(
Hello,
Hello)',
- '
Hello.remove()'
+ '
.appendChild(
Hello)'
]);
});
@@ -647,7 +647,6 @@ describe('Fragment', () => {
});
it('should preserve order for fragment switching', () => {
- /** @type {(newState: { isLoading: boolean; data: number | null }) => void} */
let set;
class Foo extends Component {
constructor(props) {
@@ -678,84 +677,6 @@ describe('Fragment', () => {
);
});
- it('should preserve order for fragment switching with sibling DOM', () => {
- /** @type {(newState: { isLoading: boolean; data: number | null }) => void} */
- let set;
- class Foo extends Component {
- constructor(props) {
- super(props);
- this.state = { isLoading: true, data: null };
- set = this.setState.bind(this);
- }
- render(props, { isLoading, data }) {
- return (
-
- HEADER
- {isLoading ? Loading...
: null}
- {data ? Content: {data}
: null}
- FOOTER
-
- );
- }
- }
-
- render(
, scratch);
- expect(scratch.innerHTML).to.equal(
- '
HEADER
Loading...
FOOTER
'
- );
-
- set({ isLoading: false, data: 2 });
- rerender();
- expect(scratch.innerHTML).to.equal(
- '
HEADER
Content: 2
FOOTER
'
- );
- });
-
- it('should preserve order for fragment switching with sibling Components', () => {
- /** @type {(newState: { isLoading: boolean; data: number | null }) => void} */
- let set;
- class Foo extends Component {
- constructor(props) {
- super(props);
- this.state = { isLoading: true, data: null };
- set = this.setState.bind(this);
- }
- render(props, { isLoading, data }) {
- return (
-
- HEADER
- {isLoading ? Loading...
: null}
- {data ? Content: {data}
: null}
-
- );
- }
- }
-
- function Footer() {
- return
FOOTER
;
- }
-
- function App() {
- return (
-
-
-
-
- );
- }
-
- render(
, scratch);
- expect(scratch.innerHTML).to.equal(
- '
HEADER
Loading...
FOOTER
'
- );
-
- set({ isLoading: false, data: 2 });
- rerender();
- expect(scratch.innerHTML).to.equal(
- '
HEADER
Content: 2
FOOTER
'
- );
- });
-
it('should preserve order for nested fragment switching w/ child return', () => {
let set;
const Wrapper = ({ children }) =>
{children};
@@ -922,12 +843,12 @@ describe('Fragment', () => {
expect(ops).to.deep.equal([]); // Component should not have updated (empty op log)
expect(scratch.innerHTML).to.equal(html);
expectDomLogToBe([
+ '
1.remove()',
+ 'Hello.remove()',
'
.appendChild(#text)',
- '1Hello2.insertBefore(
1, 1)',
+ '2.insertBefore(
1, 2)',
'.appendChild(#text)',
- '
11Hello2.insertBefore(
Hello,
1)',
- '1.remove()',
- 'Hello.remove()'
+ '
12.insertBefore(
Hello,
2)'
]);
clearLog();
@@ -936,12 +857,12 @@ describe('Fragment', () => {
expect(ops).to.deep.equal([]); // Component should not have updated (empty op log)
expect(scratch.innerHTML).to.equal(html);
expectDomLogToBe([
+ '1.remove()',
+ 'Hello.remove()',
'
.appendChild(#text)',
- '1Hello2.insertBefore(
1, 1)',
+ '2.insertBefore(
1, 2)',
'.appendChild(#text)',
- '
11Hello2.insertBefore(
Hello,
1)',
- '1.remove()',
- 'Hello.remove()'
+ '
12.insertBefore(
Hello,
2)'
]);
});
@@ -1004,9 +925,9 @@ describe('Fragment', () => {
expect(scratch.innerHTML).to.equal('foobar');
expectDomLogToBe([
- 'spamfoobar.insertBefore(#text, #text)',
'#text.remove()',
- '#text.remove()'
+ '#text.remove()',
+ '
foo.appendChild(#text)'
]);
});
@@ -1528,14 +1449,14 @@ describe('Fragment', () => {
'rendering from false to true'
);
expectDomLogToBe([
+ // Remove 3 & 4 (replaced by null)
+ '
3.remove()',
+ '4.remove()',
// Insert 0 and 1
'.appendChild(#text)',
- '2234.insertBefore(- 0,
- 2)',
+ '
22.insertBefore(- 0,
- 2)',
'
- .appendChild(#text)',
- '
02234.insertBefore(- 1,
- 2)',
- // Remove 3 & 4 (replaced by null)
- '
- 3.remove()',
- '
- 4.remove()'
+ '
022.insertBefore(- 1,
- 2)'
]);
});
@@ -1599,14 +1520,14 @@ describe('Fragment', () => {
'rendering from false to true'
);
expectDomLogToBe([
+ // Remove 4 & 5 (replaced by null)
+ '
- 4.remove()',
+ '
- 5.remove()',
// Insert 0 and 1 back into the DOM
'
- .appendChild(#text)',
- '
2345.insertBefore(- 0,
- 2)',
+ '
23.insertBefore(- 0,
- 2)',
'
- .appendChild(#text)',
- '
02345.insertBefore(- 1,
- 2)',
- // Remove 4 & 5 (replaced by null)
- '
- 4.remove()',
- '
- 5.remove()'
+ '
023.insertBefore(- 1,
- 2)'
]);
});
@@ -1660,10 +1581,10 @@ describe('Fragment', () => {
);
expectDomLogToBe(
[
- '
barHellobeepboop.insertBefore(
bar,
beep)',
- '
Hellobarbeepboop.insertBefore(
Hello,
boop)',
- '
barbeepHelloboop.insertBefore(
bar,
boop)',
- '
boop.remove()'
+ '
boop.remove()',
+ '
barHellobeep.insertBefore(
bar,
beep)',
+ '
Hellobarbeep.appendChild(
Hello)',
+ '
barbeepHello.appendChild(
bar)'
],
'rendering from true to false'
);
@@ -1855,12 +1776,12 @@ describe('Fragment', () => {
expect(scratch.innerHTML).to.equal(htmlForFalse);
expectDomLogToBe(
[
+ '
2.remove()',
+ '#text.remove()',
'
.appendChild(#text)',
- '
1.insertBefore(
3, #text)',
+ '
.appendChild(
3)',
'
.appendChild(#text)',
- '
31.insertBefore(
4, #text)',
- '#text.remove()',
- '
2.remove()'
+ '
3.appendChild(
4)'
],
'rendering from true to false'
);
@@ -1871,9 +1792,9 @@ describe('Fragment', () => {
expect(scratch.innerHTML).to.equal(htmlForTrue);
expectDomLogToBe(
[
- '
34.insertBefore(#text,
3)',
- '
4.remove()',
'
3.remove()',
+ '
4.remove()',
+ '
.appendChild(#text)',
'
.appendChild(#text)',
'
1.appendChild(
2)'
],
@@ -2135,9 +2056,9 @@ describe('Fragment', () => {
`
`
);
expectDomLogToBe([
+ '
B1.remove()',
'
.appendChild(#text)',
- 'AB1C.insertBefore(
B2, B1)',
- '
B1.remove()'
+ '
AC.insertBefore(
B2, C)'
]);
});
@@ -2185,12 +2106,12 @@ describe('Fragment', () => {
div([div('A'), section('B3'), section('B4'), div('C')])
);
expectDomLogToBe([
+ '
B1.remove()',
+ '
B2.remove()',
'
.appendChild(#text)',
- 'AB1B2C.insertBefore(
B3, B1)',
+ '
AC.insertBefore(
B3, C)',
'
.appendChild(#text)',
- 'AB3B1B2C.insertBefore(
B4, B1)',
- '
B2.remove()',
- '
B1.remove()'
+ '
AB3C.insertBefore(
B4, C)'
]);
});
@@ -2503,9 +2424,9 @@ describe('Fragment', () => {
expect(scratch.innerHTML).to.eql(`
`);
expectDomLogToBe([
+ '
A.remove()',
'
.appendChild(#text)',
- 'AC.insertBefore(
A2, A)',
- '
A.remove()'
+ '
C.insertBefore(
A2, C)'
]);
});
@@ -2570,12 +2491,12 @@ describe('Fragment', () => {
`
`
);
expectDomLogToBe([
+ '
A1.remove()',
+ '
A2.remove()',
'
.appendChild(#text)',
- 'A1A2C.insertBefore(
A3, A1)',
+ '
C.insertBefore(
A3, C)',
'
.appendChild(#text)',
- 'A3A1A2C.insertBefore(
A4, A1)',
- '
A2.remove()',
- '
A1.remove()'
+ '
A3C.insertBefore(
A4, C)'
]);
});
@@ -2651,9 +2572,9 @@ describe('Fragment', () => {
'updateA'
);
expectDomLogToBe([
+ '
A.remove()',
'
.appendChild(#text)',
- 'ABC.insertBefore(
A2, A)',
- '
A.remove()'
+ '
BC.insertBefore(
A2, B)'
]);
});
@@ -2708,12 +2629,12 @@ describe('Fragment', () => {
'updateA'
);
expectDomLogToBe([
+ '
A1.remove()',
+ '
A2.remove()',
'
.appendChild(#text)',
- 'A1A2.insertBefore(
A3, A1)',
+ '
.appendChild(
A3)',
'.appendChild(#text)',
- 'A3A1A2.insertBefore(
A4, A1)',
- '
A2.remove()',
- '
A1.remove()'
+ '
A3.appendChild(
A4)'
]);
clearLog();
@@ -2731,7 +2652,8 @@ describe('Fragment', () => {
});
it('should properly place conditional elements around strictly equal vnodes', () => {
- let set;
+ /** @type {() => void} */
+ let toggle;
const Children = () => (
@@ -2744,7 +2666,7 @@ describe('Fragment', () => {
constructor(props) {
super(props);
this.state = { panelPosition: 'bottom' };
- set = this.tooglePanelPosition = this.tooglePanelPosition.bind(this);
+ toggle = this.tooglePanelPosition = this.tooglePanelPosition.bind(this);
}
tooglePanelPosition() {
@@ -2778,17 +2700,17 @@ describe('Fragment', () => {
expect(scratch.innerHTML).to.equal(bottom);
clearLog();
- set();
+ toggle();
rerender();
expect(scratch.innerHTML).to.equal(top);
expectDomLogToBe([
+ 'bottom panel.remove()',
'
.appendChild(#text)',
- '
NavigationContentbottom panel.insertBefore(
top panel,
Navigation)',
- '
bottom panel.remove()'
+ '
NavigationContent.insertBefore(
top panel,
Navigation)'
]);
clearLog();
- set();
+ toggle();
rerender();
expect(scratch.innerHTML).to.equal(bottom);
expectDomLogToBe([
@@ -2798,13 +2720,13 @@ describe('Fragment', () => {
]);
clearLog();
- set();
+ toggle();
rerender();
expect(scratch.innerHTML).to.equal(top);
expectDomLogToBe([
+ '
bottom panel.remove()',
'
.appendChild(#text)',
- '
NavigationContentbottom panel.insertBefore(
top panel,
Navigation)',
- '
bottom panel.remove()'
+ '
NavigationContent.insertBefore(
top panel,
Navigation)'
]);
});
@@ -3020,10 +2942,10 @@ describe('Fragment', () => {
div([div(1), div(4), div('A'), div('B')])
);
expectDomLogToBe([
- '
.appendChild(#text)',
- '
123AB.insertBefore(
4,
2)',
'
2.remove()',
- '
3.remove()'
+ '
3.remove()',
+ '
.appendChild(#text)',
+ '
1AB.insertBefore(
4,
A)'
]);
});
@@ -3118,11 +3040,11 @@ describe('Fragment', () => {
expect(scratch.innerHTML).to.equal(div([span(1), div('A'), div('B')]));
expectDomLogToBe([
- '
.appendChild(#text)',
- '