riot/dom-bindings

View on GitHub
src/bindings/each.js

Summary

Maintainability
A
2 hrs
Test Coverage
import { insertBefore, removeChild } from '@riotjs/util/dom.js'
import { defineProperty } from '@riotjs/util/objects.js'
import { isTemplate } from '@riotjs/util/checks.js'
import createTemplateMeta from '../util/create-template-meta.js'
import udomdiff from '../util/udomdiff.js'

const UNMOUNT_SCOPE = Symbol('unmount')

export const EachBinding = {
  // dynamic binding properties
  // childrenMap: null,
  // node: null,
  // root: null,
  // condition: null,
  // evaluate: null,
  // template: null,
  // isTemplateTag: false,
  nodes: [],
  // getKey: null,
  // indexName: null,
  // itemName: null,
  // afterPlaceholder: null,
  // placeholder: null,

  // API methods
  mount(scope, parentScope) {
    return this.update(scope, parentScope)
  },
  update(scope, parentScope) {
    const { placeholder, nodes, childrenMap } = this
    const collection = scope === UNMOUNT_SCOPE ? null : this.evaluate(scope)
    const items = collection ? Array.from(collection) : []

    // prepare the diffing
    const { newChildrenMap, batches, futureNodes } = createPatch(
      items,
      scope,
      parentScope,
      this,
    )

    // patch the DOM only if there are new nodes
    udomdiff(
      nodes,
      futureNodes,
      patch(Array.from(childrenMap.values()), parentScope),
      placeholder,
    )

    // trigger the mounts and the updates
    batches.forEach((fn) => fn())

    // update the children map
    this.childrenMap = newChildrenMap
    this.nodes = futureNodes

    return this
  },
  unmount(scope, parentScope) {
    this.update(UNMOUNT_SCOPE, parentScope)

    return this
  },
}

/**
 * Patch the DOM while diffing
 * @param   {any[]} redundant - list of all the children (template, nodes, context) added via each
 * @param   {*} parentScope - scope of the parent template
 * @returns {Function} patch function used by domdiff
 */
function patch(redundant, parentScope) {
  return (item, info) => {
    if (info < 0) {
      // get the last element added to the childrenMap saved previously
      const element = redundant[redundant.length - 1]

      if (element) {
        // get the nodes and the template in stored in the last child of the childrenMap
        const { template, nodes, context } = element
        // remove the last node (notice <template> tags might have more children nodes)
        nodes.pop()

        // notice that we pass null as last argument because
        // the root node and its children will be removed by domdiff
        if (!nodes.length) {
          // we have cleared all the children nodes and we can unmount this template
          redundant.pop()
          template.unmount(context, parentScope, null)
        }
      }
    }

    return item
  }
}

/**
 * Check whether a template must be filtered from a loop
 * @param   {Function} condition - filter function
 * @param   {Object} context - argument passed to the filter function
 * @returns {boolean} true if this item should be skipped
 */
function mustFilterItem(condition, context) {
  return condition ? !condition(context) : false
}

/**
 * Extend the scope of the looped template
 * @param   {Object} scope - current template scope
 * @param   {Object} options - options
 * @param   {string} options.itemName - key to identify the looped item in the new context
 * @param   {string} options.indexName - key to identify the index of the looped item
 * @param   {number} options.index - current index
 * @param   {*} options.item - collection item looped
 * @returns {Object} enhanced scope object
 */
function extendScope(scope, { itemName, indexName, index, item }) {
  defineProperty(scope, itemName, item)
  if (indexName) defineProperty(scope, indexName, index)

  return scope
}

/**
 * Loop the current template items
 * @param   {Array} items - expression collection value
 * @param   {*} scope - template scope
 * @param   {*} parentScope - scope of the parent template
 * @param   {EachBinding} binding - each binding object instance
 * @returns {Object} data
 * @returns {Map} data.newChildrenMap - a Map containing the new children template structure
 * @returns {Array} data.batches - array containing the template lifecycle functions to trigger
 * @returns {Array} data.futureNodes - array containing the nodes we need to diff
 */
function createPatch(items, scope, parentScope, binding) {
  const {
    condition,
    template,
    childrenMap,
    itemName,
    getKey,
    indexName,
    root,
    isTemplateTag,
  } = binding
  const newChildrenMap = new Map()
  const batches = []
  const futureNodes = []

  items.forEach((item, index) => {
    const context = extendScope(Object.create(scope), {
      itemName,
      indexName,
      index,
      item,
    })
    const key = getKey ? getKey(context) : index
    const oldItem = childrenMap.get(key)
    const nodes = []

    if (mustFilterItem(condition, context)) {
      return
    }

    const mustMount = !oldItem
    const componentTemplate = oldItem ? oldItem.template : template.clone()
    const el = componentTemplate.el || root.cloneNode()
    const meta =
      isTemplateTag && mustMount
        ? createTemplateMeta(componentTemplate)
        : componentTemplate.meta

    if (mustMount) {
      batches.push(() =>
        componentTemplate.mount(el, context, parentScope, meta),
      )
    } else {
      batches.push(() => componentTemplate.update(context, parentScope))
    }

    // create the collection of nodes to update or to add
    // in case of template tags we need to add all its children nodes
    if (isTemplateTag) {
      nodes.push(...meta.children)
    } else {
      nodes.push(el)
    }

    // delete the old item from the children map
    childrenMap.delete(key)
    futureNodes.push(...nodes)

    // update the children map
    newChildrenMap.set(key, {
      nodes,
      template: componentTemplate,
      context,
      index,
    })
  })

  return {
    newChildrenMap,
    batches,
    futureNodes,
  }
}

export default function create(
  node,
  { evaluate, condition, itemName, indexName, getKey, template },
) {
  const placeholder = document.createTextNode('')
  const root = node.cloneNode()

  insertBefore(placeholder, node)
  removeChild(node)

  return {
    ...EachBinding,
    childrenMap: new Map(),
    node,
    root,
    condition,
    evaluate,
    isTemplateTag: isTemplate(root),
    template: template.createDOM(node),
    getKey,
    indexName,
    itemName,
    placeholder,
  }
}