All files / lib/tasks reconcile-trees.js

100% Statements 122/122
100% Branches 22/22
100% Functions 1/1
100% Lines 122/122

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 1235x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 160x 160x 160x 160x 150x 150x 150x 150x 150x 150x 150x 160x 160x 160x 160x 160x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 231x 4x 4x 231x 3x 3x 3x 3x 6x 6x 6x 6x 6x 6x 3x 3x 6x 3x 3x 3x 1x 1x 2x 2x 1x 1x 3x 231x 231x 231x 231x 231x 69x 69x 69x 69x 231x 231x 231x 231x 231x 231x 231x 231x 231x 177x 177x 177x 177x 177x 177x 231x  
import { StateCache, NODE_TYPE, VTree } from '../util/types';
import { protectVTree } from '../util/memory';
import createTree from '../tree/create';
import Transaction from '../transaction';
import release from '../release';
 
/**
 * This task ensures that the Virtual DOM matches the Browser DOM. If any of
 * the markup changes between renders, the old tree is recalculated to ensure
 * accuracy.
 *
 * @param {Transaction} transaction
 */
export default function reconcileTrees(transaction) {
  const { state, mount, input, config: options } = transaction;
  const { inner } = options;
  const mountAsHTMLEl = /** @type {HTMLElement} */ (mount);
  const inputAsVTree = /** @type {VTree} */(input);
 
  // Look if any changes happened before the async mutation callback.
  if (state.mutationObserver && !state.isDirty) {
    state.isDirty = Boolean(state.mutationObserver.takeRecords().length);
  }
  // If no mutation observer exists, then we cannot determine if the root is
  // dirty. Disable the isDirty check in these cases.
  else if (!state.mutationObserver) {
    state.isDirty = false;
  }
 
  // We rebuild the tree whenever the DOM Node changes, including the first
  // time we patch a DOM Node. This is currently problematic when someone
  // passes a DOM Node as an interpolated value we empty the VTree of its
  // contents, but the newTree still has a reference and is expecting
  // populated values.
  if (state.isDirty || !state.oldTree) {
    release(mountAsHTMLEl);
 
    // Ensure the mutation observer is reconnected.
    if (mountAsHTMLEl.ownerDocument && state.mutationObserver) {
      state.mutationObserver.observe(mountAsHTMLEl, {
        subtree: true,
        childList: true,
        attributes: true,
        characterData: true,
      });
    }
 
    state.oldTree = createTree(mountAsHTMLEl);
    protectVTree(state.oldTree);
    StateCache.set(mount, state);
  }
 
  const { nodeName, attributes } = state.oldTree;
 
  // TODO When `inner === false` this means we are doing outerHTML operation.
  // The way this works is that anything that doesn't match the oldTree element
  // gets diffed internally. Anything that matches the root element at the top
  // level gets absorbed into the root element. Order is not important. Elements
  // which are matched subsequently are merged, but only the first occurance of
  // an attribute is counted. The rules are complicated, but if we match the
  // browser behavior here, it will be significantly easier to convince of it's
  // validity and to document.
 
  // To mimic browser behavior, we loop the input and take any tree that matches
  // the root element and unwrap into the root element. We take the attributes
  // from that element and apply to the root element. This ultimately renders a
  // flat tree and allows for whitespace to be provided in the `html` function
  // without needing to trim.
  if (
    !inner &&
    inputAsVTree.nodeType === NODE_TYPE.FRAGMENT &&
    // Do not modify the new children when comparing two fragments.
    state.oldTree.nodeType !== NODE_TYPE.FRAGMENT
  ) {
    /** @type {VTree[]} */
    let foundElements = [];
 
    for (let i = 0; i < inputAsVTree.childNodes.length; i++) {
      const value = inputAsVTree.childNodes[i];
      const isText = value.nodeType === NODE_TYPE.TEXT;
 
      // This is most likely the element that is requested to compare to. Will
      // need to keep checking or more input though to be totally sure.
      if (!isText || value.nodeValue.trim()) {
        foundElements.push(value);
      }
    }
 
    // If only one element is found, we can use this directly.
    if (foundElements.length === 1) {
      transaction.newTree = foundElements[0];
    }
    // Otherwise consider the entire fragment.
    else if (foundElements.length > 1) {
      transaction.newTree = createTree(inputAsVTree.childNodes);
    }
  }
 
  // If we are in a render transaction where no markup was previously parsed
  // then reconcile trees will attempt to create a tree based on the incoming
  // markup (JSX/html/etc).
  if (!transaction.newTree) {
    // Reset the old tree with the newly created VTree association.
    transaction.newTree = createTree(input);
 
  }
 
  // Associate the old tree with this brand new transaction.
  transaction.oldTree = state.oldTree;
 
  const { oldTree, newTree } = transaction;
 
  // If we are diffing only the parent's childNodes, then adjust the newTree to
  // be a replica of the oldTree except with the childNodes changed.
  if (inner && oldTree && newTree) {
    const isUnknown = typeof newTree.rawNodeName !== 'string';
    const isFragment = newTree.nodeType === NODE_TYPE.FRAGMENT;
    const children = isFragment && !isUnknown ? newTree.childNodes : newTree;
 
    transaction.newTree = createTree(nodeName, attributes, children);
  }
}