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

com.raquo.laminar.setters.ChildrenSetter.scala Maven / Gradle / Ivy

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

import com.raquo.airstream.core.Observable
import com.raquo.airstream.eventstream.EventStream
import com.raquo.airstream.signal.Signal
import com.raquo.airstream.state.State
import com.raquo.domtypes.generic.Modifier
import com.raquo.laminar.DomApi
import com.raquo.laminar.nodes.{ReactiveChildNode, ReactiveComment, ReactiveElement}
import com.raquo.laminar.setters.ChildrenSetter.Children
import org.scalajs.dom

import scala.collection.immutable

class ChildrenSetter(
  childrenObservable: Observable[Children]
) extends Modifier[ReactiveElement[dom.Element]] {

  import ChildrenSetter.{emptyChildren, updateChildren}

  override def apply(parentNode: ReactiveElement[dom.Element]): Unit = {
    var nodeCount = 0

    val sentinelNode = new ReactiveComment("")
    parentNode.appendChild(sentinelNode)(DomApi.treeApi)

    val childrenSignal = childrenObservable match {
      case stream: EventStream[Children @unchecked] => stream.toSignal(emptyChildren)
      case state: State[Children @unchecked] => state.toSignal
      case signal: Signal[Children @unchecked] => signal
    }

    val childrenDiffSignal = childrenSignal
      .fold[(Children, Children)](initial => (emptyChildren, initial))((diff, nextChildren) => (diff._2, nextChildren))

    parentNode.subscribe(childrenDiffSignal) { childrenDiff =>
      nodeCount = updateChildren(
        prevChildren = childrenDiff._1,
        nextChildren = childrenDiff._2,
        parentNode = parentNode,
        sentinelNode = sentinelNode,
        nodeCount
      )
    }
  }
}

object ChildrenSetter {

  private val emptyChildren = Vector()

  type Child = ReactiveChildNode[dom.Node]
  type Children = immutable.Seq[Child]

  /** @return New child node count */
  protected def updateChildren(
    prevChildren: Children,
    nextChildren: Children,
    parentNode: ReactiveElement[dom.Element],
    sentinelNode: ReactiveChildNode[dom.Node],
    prevChildrenCount: Int
  ): Int = {
    val liveNodeList = parentNode.ref.childNodes
    val sentinelIndex = parentNode.indexOfChild(sentinelNode)

    // Loop variables
    var index = 0
    var currentChildrenCount = prevChildrenCount
    var prevChildRef = liveNodeList(sentinelIndex + 1)

    // @TODO Remove all the debug comments

    //    dom.console.log(">>>>>>>>>>>>>>>>>")
    //    dom.console.log(">>>>>>>>>>>>>>>>>")

    nextChildren.foreach { nextChild =>

      /** Desired index of `nextChild` in `liveNodeList` */
      val nextChildNodeIndex = sentinelIndex + index + 1

      //      dom.console.log("\nevaluating index=" + index + ", nextChildNodeIndex=" + nextChildNodeIndex + ", prevChildRef=" + (if (prevChildRef == 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] prevChildren are not needed here, it's fixable right now by just looking at parentNode's maybeChildren

      // @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`

      if (currentChildrenCount <= index) {
        // Note: `prevChildRef` is not valid in this branch
        //        dom.console.log("INSERTING " + nextChild.ref.textContent + " at index-" + index + " (nextChildNodeIndex=" + nextChildNodeIndex + ")")
        // We ran through the whole prevChildren list already, we just need to append all remaining nextChild-s into the DOM
        parentNode.insertChild(nextChild, atIndex = nextChildNodeIndex)(DomApi.treeApi)
        // 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
        prevChildRef = nextChild.ref
        currentChildrenCount += 1
      } else {
        if (nextChild.ref == prevChildRef) {
          //          dom.console.log("NODE MATCHES – " + nextChild.ref.textContent)
          // Child nodes already match – do nothing, go to the next child
        } else {
          //          dom.console.log("NODE DOES NOT MATCH – " + nextChild.ref.textContent + " vs " + prevChildRef.textContent)

          //          dom.console.log(parentNode.ref.childNodes(0), parentNode.ref.childNodes(0).textContent)
          //          dom.console.log(parentNode.ref.childNodes(1), parentNode.ref.childNodes(1).textContent)
          //          dom.console.log(parentNode.ref.childNodes(2), parentNode.ref.childNodes(2).textContent)
          //          dom.console.log(parentNode.ref.childNodes(3), parentNode.ref.childNodes(3).textContent)
          //          dom.console.log(parentNode.ref.childNodes(4), parentNode.ref.childNodes(4).textContent)

          if (!prevChildren.contains(nextChild)) {
            // nextChild not found in prevChildren, so it's a new child, so we need to insert it
            parentNode.insertChild(nextChild, atIndex = nextChildNodeIndex)(DomApi.treeApi)
            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
            // - `nextChild.ref != prevChildNode` is a performance shortcut
            // - 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(nextChildren, prevChildRef, startLookingAtIndex = index)
            ) {
              // prevChild should be deleted, so we remove it from the DOM, and try again with the next prevChild
              // but first we save its next sibling, which will become our next `prevChildRef`
              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)
              parentNode.removeChild(prevChild)(DomApi.treeApi)

              currentChildrenCount -= 1
              // Update prevChildNode reference
              prevChildRef = nextPrevChildRef
            }
            if (nextChild.ref != prevChildRef) {
              // nextChild is still not in the right place, so let's move it to the correct index
              parentNode.insertChild(nextChild, atIndex = nextChildNodeIndex)(DomApi.treeApi) // MOVE, so we DO NOT update currentDomChildrenCount
              prevChildRef = nextChild.ref
            }
          }
        }
      }
      prevChildRef = prevChildRef.nextSibling
      index += 1 // Faster than zipWithIndex
    }

    while (index < currentChildrenCount) {

      //      dom.console.log("\nDELETING REMAINDER: index=" + index + ", currentChildrenCount = " + currentChildrenCount)
      //      dom.console.log("prevChildRef: " + prevChildRef.textContent)

      val nextPrevChildRef = prevChildRef.nextSibling
      parentNode.removeChild(prevChildFromRef(prevChildren, prevChildRef))(DomApi.treeApi)
      prevChildRef = nextPrevChildRef
      currentChildrenCount -= 1
    }

    currentChildrenCount
  }

  protected def containsRef(nextChildren: Children, ref: dom.Node, startLookingAtIndex: Int): Boolean = {
    // @TODO[Performance] This also can be optimized for different `Seq` implementations
    val childrenCount = nextChildren.size
    var index = startLookingAtIndex
    var found = false
    while (!found && index < childrenCount) {
      if (nextChildren(index).ref == ref) {
        found = true
      } else {
        index += 1
      }
    }
    found
  }

  protected def prevChildFromRef(prevChildren: Children, ref: dom.Node): Child = {
    //    println("> prevChildFromDomNode")
    //    dom.console.log(prevChildren(0))
    //    dom.console.log(prevChildren(1))
    //    dom.console.log(ref, ref.textContent)
    prevChildren.find(_.ref == ref).get // @TODO[Integrity] Throw a more meaningful error (that would be unrecoverable inconsistent state)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy