All files / lib/core/renderer bindingScope.js

100% Statements 108/108
100% Branches 11/11
100% Functions 3/3
100% Lines 108/108

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 109505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 98x 6x 6x 92x 92x 92x 92x 92x 92x 92x 98x 505x 505x 505x 505x 505x 505x 505x 505x 505x 13x 13x 13x 13x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 1920x 1920x 8621x 8621x 21x 21x 8600x 8600x 1899x 1920x  
/**
 * @file bindingScope.js
 * @description Which scope a piece of DOM was rendered under.
 *
 * ## The problem this solves
 *
 * Most of a component's DOM is rendered against the component's own scope, and
 * an executable binding on it — an event handler, a directive — can be resolved
 * against that scope whenever it fires. Some of it is not. A `<@for>` body is
 * rendered once per item, against a scope carrying that item and its index; a
 * scoped slot is rendered against the props the parent passed. In both cases the
 * text interpolated into the markup was resolved correctly at render time, and
 * the handler *attached* to it was not: handlers are bound in one pass over the
 * component's element, with the component's scope, long after the derived scope
 * that produced the row has gone out of scope in the renderer.
 *
 * The visible result was that `@click="select(item.id)"` — the form the events
 * guide documents — threw `Cannot read property "id" of undefined`, because by
 * the time the click arrived nothing remembered which row it came from.
 *
 * ## What is stored, and where
 *
 * The scope itself, on the root element of the subtree it produced. Resolution
 * walks up from the element the handler is on and takes the nearest one, which
 * gives nesting for free: an inner `<@for>` stamps a scope derived from the
 * outer one, so a handler in the inner row finds the inner scope and reads both
 * loop variables through it.
 *
 * Storing the scope rather than a copy of its bindings is what keeps a reused
 * row correct. The list manager recycles DOM nodes, so the element a handler is
 * attached to may have held a different item a moment ago; re-stamping on every
 * create-or-patch means the answer is always the item the element holds now.
 *
 * ## Why a property rather than a WeakMap
 *
 * A WeakMap keyed by node would be tidier, but resolution walks ancestors on
 * every dispatched event and a property read is the cheaper of the two by
 * enough to matter on a large list. The property is non-enumerable so it does
 * not appear in a serialised node, a diff or a trace.
 * @module lib/core/renderer/bindingScope
 */
 
/**
 * The property holding a subtree's scope.
 *
 * Named for what it is rather than for the first thing that needed it. Scoped
 * slots got here first and called it `__avenx_slot_scope`; lists, defer blocks
 * and error fallbacks all need exactly the same thing, and a list row carrying
 * something called a slot scope reads as a bug to whoever finds it next.
 * @type {string}
 */
const SCOPE_PROPERTY = '__avenx_binding_scope';
 
/**
 * Records the scope a subtree was rendered under.
 *
 * Safe to call repeatedly on the same node: a recycled list row is re-stamped
 * with the item it now holds.
 * @param {Node} node - The root of the rendered subtree.
 * @param {object|null} scope - The scope it was rendered against.
 * @returns {Node} The node, for chaining.
 */
export function stampScope(node, scope) {
  if (!node || node.nodeType !== 1 || !scope) {
    return node;
  }
  Object.defineProperty(node, SCOPE_PROPERTY, {
    value: scope,
    configurable: true,
    enumerable: false,
    writable: true,
  });
  return node;
}
 
/**
 * Removes a scope stamp.
 *
 * Called when a node is returned to the list manager's pool, so a pooled node
 * cannot hand a stale item to anything that reads it before it is re-stamped.
 * @param {Node} node - The node to clear.
 */
export function clearScope(node) {
  if (node && node.nodeType === 1 && Object.prototype.hasOwnProperty.call(node, SCOPE_PROPERTY)) {
    delete node[SCOPE_PROPERTY];
  }
}
 
/**
 * The scope an element's bindings should resolve against.
 *
 * Walks ancestors and returns the nearest stamp, so the innermost enclosing
 * derived scope wins. Returns null when the element sits in ordinary component
 * DOM, which means "resolve against the component scope" to every caller.
 * @param {Node|null} node - Where to start looking.
 * @returns {object|null} The nearest enclosing scope, or null.
 */
export function findScope(node) {
  let current = node;
  while (current) {
    const scope = current[SCOPE_PROPERTY];
    if (scope) {
      return scope;
    }
    current = current.parentNode;
  }
  return null;
}