All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.raquo.laminar.inserters.ChildrenInserter.scala Maven / Gradle / Ivy

The newest version!
package com.raquo.laminar.inserters

import com.raquo.airstream.core.Observable
import com.raquo.ew.JsMap
import com.raquo.laminar.modifiers.RenderableNode
import com.raquo.laminar.nodes.{ChildNode, ParentNode, ReactiveElement}
import org.scalajs.dom

import scala.collection.immutable
import scala.scalajs.js

object ChildrenInserter {

  @deprecated("`Child` type alias is deprecated. Use ChildNode.Base", "15.0.0-M6")
  type Child = ChildNode.Base

  @deprecated("`Children`type alias is deprecated. Use immutable.Seq[ChildNode.Base]", "15.0.0-M6")
  type Children = immutable.Seq[ChildNode.Base]

  def apply[Component](
    childrenSource: Observable[immutable.Seq[Component]],
    renderableNode: RenderableNode[Component],
    initialHooks: js.UndefOr[InserterHooks]
  ): DynamicInserter = {
    new DynamicInserter(
      preferStrictMode = true,
      insertFn = (ctx, owner, hooks) => {
        // Reset sentinel node on binding too, don't wait for events
        if (!ctx.strictMode) {
          ctx.forceSetStrictMode()
        }
        // var maybeLastSeenChildren: js.UndefOr[immutable.Seq[ChildNode.Base]] = ctx.extraNodes
        childrenSource.foreach { components =>
          // #TODO[Performance] This is not ideal – for CUSTOM renderable components asNodeSeq
          //  will need to map over the seq, creating a new seq of child nodes.
          //  Unfortunately, avoiding this is quite complicated.
          val newChildren = renderableNode.asNodeSeq(components)

          // #TODO[Performance] Consider bringing back this eq check. Benchmark performance cost.
          //  - Without it, we might get worse performance, as when the same list is emitted,
          //    Laminar needs to iterate over the list of elements and check their position in the DOM.
          //    These checks are desirable in rare cases when the list of elements is affected by other
          //    inserters, e.g. when another inserter has removed an item from the list, and later this
          //    inserter re-emits the same list trying to add the item back where it used to be.
          //  - TLDR – we're choosing correctness over performance for now, but both are only a small
          //    difference. Check that performance cost is not too bad with benchmarks.

          // if (!maybeLastSeenChildren.exists(_ eq newChildren)) { // #Note: auto-distinction
          //   maybeLastSeenChildren = newChildren
          switchToChildren(newChildren, ctx, hooks)
          // }
        }(owner)
      },
      hooks = initialHooks
    )
  }

  def switchToChildren(
    newChildren: immutable.Seq[ChildNode.Base],
    ctx: InsertContext,
    hooks: js.UndefOr[InserterHooks]
  ): Unit = {
    if (!ctx.strictMode) {
      // #Note: previously in ChildInserter we only did this once in insertFn.
      //  I think it's cheap and safe to do this check on every childSource.foreach.
      ctx.forceSetStrictMode()
    }

    val newChildrenMap = InsertContext.nodesToMap(newChildren)
    ctx.extraNodeCount = updateChildren(
      prevChildren = ctx.extraNodesMap,
      nextChildren = newChildren,
      nextChildrenMap = newChildrenMap,
      parentNode = ctx.parentNode,
      sentinelNode = ctx.sentinelNode,
      ctx.extraNodeCount,
      hooks
    )
    ctx.extraNodes = newChildren
    ctx.extraNodesMap = newChildrenMap
  }

  /** @return New child node count */
  private def updateChildren(
    prevChildren: JsMap[dom.Node, ChildNode.Base],
    nextChildren: immutable.Seq[ChildNode.Base],
    nextChildrenMap: JsMap[dom.Node, ChildNode.Base],
    parentNode: ReactiveElement.Base,
    sentinelNode: ChildNode.Base,
    prevChildrenCount: Int,
    hooks: js.UndefOr[InserterHooks]
  ): Int = {

    // Loop variables
    var index = 0
    var currentChildrenCount = prevChildrenCount
    var prevChildRef = sentinelNode.ref.nextSibling

    // Sorry for all the debug comments, but they really help me figure things out.

    // println(">>>>>>>>>>>>>>>>>")
    // println(s"updateChildren(nextChildren = ${nextChildren.map(_.ref.textContent)})")

    var lastIndexChild = sentinelNode

    nextChildren.foreach { nextChild => // #TODO Not sure if this is faster than iterating over a js.Map

      // println("evaluating index=" + index + ", nextChildNodeIndex=" + nextChildNodeIndex + ", prevChildRef=" + (if ((prevChildRef: js.UndefOr[dom.Node]) == js.undefined || prevChildRef == null) "null or undefined" else prevChildRef.textContent))

      // @TODO[Integrity] prevChildRef can be null or even undefined here if we reach the end, under certain circumstances. See what can be done...

      // @TODO[Performance] this diffing algo is decent, but can still be optimized in a few ways (but we need benchmarking & data for that)
      // @TODO[Performance] We could optimize this for specific `Seq` implementations. For example, foreach is faster than while() on a `List`

      // @Note: Whenever we insert, move or remove items from the DOM, we need to manually update `prevChildRef` to point to the node at the current index

      if (currentChildrenCount <= index) {
        // We ran through the whole prevChildren list already, we just need to append all remaining nextChild-s into the DOM
        // Note: `prevChildRef` is not valid in this branch
        // println("> overflow: inserting " + nextChild.ref.textContent + " at index " + nextChildNodeIndex)
        // @Note: DOM update
        // ParentNode.insertChild(parent = parentNode, child = nextChild, atIndex = nextChildNodeIndex)
        ParentNode.insertChildAfter(
          parent = parentNode,
          newChild = nextChild,
          referenceChild = lastIndexChild,
          hooks
        )
        // println(s"setting prevChildRef=${nextChild.ref.textContent}")
        prevChildRef = nextChild.ref
        currentChildrenCount += 1
      } else {
        if (nextChild.ref == prevChildRef) {
          // println("NODE MATCHES – " + nextChild.ref.textContent)
          // Child nodes already match – do nothing, go to the next child
        } else {
          // println("NODE DOES NOT MATCH")
          // println("NODE DOES NOT MATCH – " + nextChild.ref.textContent + " vs " + prevChildRef.textContent)

          if (!prevChildren.has(nextChild.ref)) {
            // nextChild not found in prevChildren, so it's a new child, so we need to insert it
            // println("> new: inserting " + nextChild.ref.textContent + " at index " + nextChildNodeIndex)
            // @Note: DOM update
            ParentNode.insertChildAfter(
              parent = parentNode,
              newChild = nextChild,
              referenceChild = lastIndexChild,
              hooks
            )
            // println(s"setting prevChildRef=${nextChild.ref.textContent}")
            prevChildRef = nextChild.ref
            currentChildrenCount += 1
          } else {
            // nextChild is found, but at a different index

            // First, let's check if prevChild should be deleted.
            // This will reduce the amount of moving needed to be done in most cases.
            // Note:
            // - This loop should never go out of bounds on `liveNodeList` because we know that `nextChild.ref` is still in that list somewhere
            // - In `containsNode` call we only start looking at `index` because we know that all nodes before `index` are already in place.
            while (
              nextChild.ref != prevChildRef && !containsRef(nextChildrenMap, prevChildRef)
            ) {
              // Loop logic:
              // - prevChild should be deleted, so we remove it from the DOM,
              //   and try again with the next prevChild in the DOM.
              // - We repeat this until we find an element in the DOM that is
              //   present in nextChildren.

              // println(s"> prevChildRef == ${if (prevChildRef == null) "null!" else prevChildRef.textContent}")
              val nextPrevChildRef = prevChildRef.nextSibling //@TODO[Integrity] See warning in https://developer.mozilla.org/en-US/docs/Web/API/Node/nextSibling (should not affect us though)

              val prevChild = prevChildFromRef(prevChildren, prevChildRef)
              // println("> removing " + prevChild.ref.textContent)
              // @Note: DOM update
              ParentNode.removeChild(
                parent = parentNode,
                child = prevChild
              )
              // println(s"setting prevChildRef=${nextPrevChildRef.textContent}")
              prevChildRef = nextPrevChildRef
              currentChildrenCount -= 1
            }
            if (nextChild.ref != prevChildRef) {
              // nextChild is still not in the right place, so let's move it to the correct index
              // println("> order: inserting " + nextChild.ref.textContent + " at index " + nextChildNodeIndex)
              // @Note: DOM update
              ParentNode.insertChildAfter(
                parent = parentNode,
                newChild = nextChild,
                referenceChild = lastIndexChild,
                hooks
              )
              prevChildRef = nextChild.ref
              // This is a MOVE, so we DO NOT update currentDomChildrenCount here.
            }
          }
        }
      }
      // println(s">> prevChildRef == ${if (prevChildRef == null) "null!" else prevChildRef.textContent}")
      // println(s"setting prevChildRef=${if (prevChildRef.nextSibling == null) "null!" else prevChildRef.nextSibling.textContent}")
      if (prevChildRef.nextSibling == null) {
        // This case is unexpected. It can happen when elements are removed from the DOM manually,
        // or when they are moved from one `children <--` list to another via standard Laminar functionality.
        // See issue: https://github.com/raquo/Laminar/issues/120
        //
        // At this point in the code, what we know that:
        // - There are no more elements in the DOM – the `nextSibling` of the last element we looked at / inserted is `null`.
        // - There are no more `nextChildren` – we've just exhausted our foreach loop above
        // Conclusion:
        // - We thought there would be more elements in the DOM, but they were removed (presumably externally),
        //   and they are not found in `nextChildren`. So everything is right, but "for the wrong reasons", sort of.
        // What we need to do:
        // - Update `currentChildrenCount` to the accurate number, since it will be used on the next update.
        // - `prevChildren` map will be discarded after this method runs, so we do NOT need to update that
        currentChildrenCount = index + 1
      } else {
        prevChildRef = prevChildRef.nextSibling
      }
      lastIndexChild = nextChild
      index += 1
    }

    // println("reached end of nextChildren")
    while (index < currentChildrenCount) {
      // We ran out of new items before we ran out of current items. Now deleting the remainder of current items.

      // println(s"index=${index}, currentChildrenCount=${currentChildrenCount}")
      // println(s">>> prevChildRef == ${if (prevChildRef == null) "null!" else prevChildRef.textContent}")
      val nextPrevChildRef = prevChildRef.nextSibling
      // Whenever we insert, move or remove items from the DOM, we need to manually update `prevChildRef` to point to the node at the current index
      // @Note: DOM update
      val prevChild = prevChildFromRef(prevChildren, prevChildRef)
      // println(s"> removing(2) ${prevChild.ref.textContent}")
      ParentNode.removeChild(parent = parentNode, child = prevChild)
      // println(s"setting(2) prevChildRef=${if (nextPrevChildRef == null) "null!" else nextPrevChildRef.textContent}")
      prevChildRef = nextPrevChildRef
      currentChildrenCount -= 1
    }

    currentChildrenCount
  }

  private def containsRef(nextChildrenMap: JsMap[dom.Node, ChildNode.Base], ref: dom.Node): Boolean = {
    nextChildrenMap.has(ref)
  }

  private def prevChildFromRef(prevChildren: JsMap[dom.Node, ChildNode.Base], ref: dom.Node): ChildNode.Base = {
    prevChildren.get(ref).getOrElse(throw new Exception(s"prevChildFromRef[children]: not found for ${ref}"))
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy