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

outwatch.interpreter.SnabbdomOps.scala Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
package outwatch.interpreter

import outwatch._
import outwatch.helpers._
import colibri._
import snabbdom._

import scala.scalajs.js

private[outwatch] object SnabbdomOps {
  private val MicrotaskExecutor = scala.scalajs.concurrent.QueueExecutionContext.promises()

  @inline private def createDataObject(modifiers: SeparatedModifiers, vNodeNS: js.UndefOr[String]): DataObject =
    new DataObject {
      attrs = modifiers.attrs
      props = modifiers.props
      style = modifiers.styles
      on = modifiers.emitters
      hook = new Hooks {
        init = modifiers.initHook
        insert = modifiers.insertHook
        prepatch = modifiers.prePatchHook
        update = modifiers.updateHook
        postpatch = modifiers.postPatchHook
        destroy = modifiers.destroyHook
      }
      key = modifiers.keyOption
      ns = vNodeNS
    }

  private def createProxy(
    modifiers: SeparatedModifiers,
    nodeType: String,
    vNodeId: js.UndefOr[Int],
    vNodeNS: js.UndefOr[String],
  ): VNodeProxy = {
    val dataObject = createDataObject(modifiers, vNodeNS)

    @inline def newProxy(childProxies: js.UndefOr[js.Array[VNodeProxy]], string: js.UndefOr[String]) = new VNodeProxy {
      sel = nodeType
      data = dataObject
      children = childProxies
      text = string
      key = modifiers.keyOption
      _id = vNodeId
      _unmount = modifiers.domUnmountHook
    }

    if (modifiers.hasOnlyTextChildren) {
      modifiers.proxies.fold(newProxy(js.undefined, js.undefined)) { proxies =>
        newProxy(js.undefined, proxies.foldLeft("")(_ + _.text))
      }
    } else newProxy(modifiers.proxies, js.undefined)
  }

  def getBaseNode(node: VNode): BasicVNode = node match {
    case n: BasicVNode       => n
    case n: ThunkVNode       => getBaseNode(n.baseNode)
    case n: ConditionalVNode => getBaseNode(n.baseNode)
    case n: SyncEffectVNode  => getBaseNode(n.unsafeRun())
  }

  def getNamespace(node: BasicVNode): js.UndefOr[String] = node match {
    case _: SvgVNode  => "http://www.w3.org/2000/svg": js.UndefOr[String]
    case _: HtmlVNode => js.undefined
  }

  def toSnabbdom(node: VNode, config: RenderConfig): VNodeProxy = node match {
    case node: BasicVNode =>
      toRawSnabbdomProxy(node, config)
    case node: ConditionalVNode =>
      val baseNode = getBaseNode(node.baseNode)
      thunk.conditional(
        getNamespace(baseNode),
        baseNode.nodeType,
        node.key,
        () => toRawSnabbdomProxy(getBaseNode(baseNode(node.renderFn(), Key(node.key))), config),
        node.shouldRender,
      )
    case node: ThunkVNode =>
      val baseNode = getBaseNode(node.baseNode)
      thunk(
        getNamespace(baseNode),
        baseNode.nodeType,
        node.key,
        () => toRawSnabbdomProxy(getBaseNode(baseNode(node.renderFn(), Key(node.key))), config),
        node.arguments,
      )
    case node: SyncEffectVNode =>
      toSnabbdom(node.unsafeRun(), config)
  }

  private val newNodeId: () => Int = {
    var vNodeIdCounter = 0
    () => {
      vNodeIdCounter += 1
      vNodeIdCounter
    }
  }

  // we are mutating the initial proxy with VNodeProxy.copyInto, because parents of this node have a reference to this proxy.
  // if we are changing the content of this proxy via a stream, the parent will not see this change.
  // if now the parent is rerendered because a sibiling of the parent triggers an update, the parent
  // renders its children again. But it would not have the correct state of this proxy. Therefore,
  // we mutate the initial proxy and thereby mutate the proxy the parent knows.
  private def toRawSnabbdomProxy(node: BasicVNode, config: RenderConfig): VNodeProxy = {

    val vNodeNS      = getNamespace(node)
    val vNodeId: Int = newNodeId()

    val observer = new StatefulObserver[Unit]

    val nativeModifiers = NativeModifiers.from(node.modifiers, config, observer)

    if (nativeModifiers.subscribables.forall(_.isEmpty())) {
      // if no dynamic/subscribable content, then just create a simple proxy
      createProxy(SeparatedModifiers.from(nativeModifiers.modifiers, config), node.nodeType, vNodeId, vNodeNS)
    } else if (nativeModifiers.hasStream) {
      // if there is streamable content, we update the initial proxy with
      // in unsafeSubscribe and unsafeUnsubscribe callbacks. We unsafeSubscribe and unsafeUnsubscribe
      // based in dom events.

      var proxy: VNodeProxy                                   = null
      var nextModifiers: js.UndefOr[js.Array[StaticVMod]]     = js.undefined
      var _prependModifiers: js.UndefOr[js.Array[StaticVMod]] = js.undefined
      var isActive: Boolean                                   = true
      var isMounted: Boolean                                  = false

      val asyncCancelable = Cancelable.variable()

      def doPatch(): Unit = if (isMounted) {
        // update the current proxy with the new state
        val separatedModifiers = SeparatedModifiers.from(
          nativeModifiers.modifiers,
          config,
          prependModifiers = _prependModifiers,
          appendModifiers = nextModifiers,
        )
        nextModifiers = separatedModifiers.nextModifiers
        val newProxy = createProxy(separatedModifiers, node.nodeType, vNodeId, vNodeNS)
        newProxy._update = proxy._update
        newProxy._args = proxy._args

        // call the snabbdom patch method to update the dom
        OutwatchTracing.patchSubject.unsafeOnNext(newProxy)
        patch(proxy, newProxy)
        ()
      }

      def cancelAsyncPatch(): Unit = {
        asyncCancelable.unsafeAddExisting(Cancelable.empty)
      }

      def asyncPatch(): Unit = if (isActive) {
        asyncCancelable.unsafeAdd { () =>
          var isCancel   = false
          val cancelable = Cancelable(() => isCancel = true)
          MicrotaskExecutor.execute(() => if (!isCancel) doPatch())
          cancelable
        }
      }

      def start(): Unit = if (!isActive) {
        isActive = true
        cancelAsyncPatch()
        nativeModifiers.subscribables.foreach(_.unsafeSubscribe())
      }

      def stop(): Unit = if (isActive) {
        isActive = false
        cancelAsyncPatch()
        nativeModifiers.subscribables.foreach(_.unsafeUnsubscribe())
      }

      // hooks for subscribing and unsubscribing the streamable content
      _prependModifiers = js.Array[StaticVMod](
        InsertHook { p =>
          VNodeProxy.copyInto(p, proxy)
          isMounted = true
          start()
        },
        PostPatchHook { (o, p) =>
          VNodeProxy.copyInto(p, proxy)
          proxy._update.foreach(_(proxy))
          if (!NativeModifiers.equalsVNodeIds(o._id, p._id)) {
            isMounted = true
            start()
          }
        },
        DomUnmountHook { _ =>
          isMounted = false
          stop()
        },
      )

      // create initial proxy, we want to apply the initial state of the
      // receivers to the node
      val separatedModifiers =
        SeparatedModifiers.from(nativeModifiers.modifiers, config, prependModifiers = _prependModifiers)
      nextModifiers = separatedModifiers.nextModifiers
      proxy = createProxy(separatedModifiers, node.nodeType, vNodeId, vNodeNS)

      // set the patch observer so on subscribable updates we get a patch call
      observer.set(
        Observer.create[Unit](
          _ => asyncPatch(),
          OutwatchTracing.errorSubject.unsafeOnNext,
        ),
      )

      proxy
    } else {
      // simpler version with only subscriptions, no streams.
      var isActive = true

      def start(): Unit = if (!isActive) {
        isActive = true
        nativeModifiers.subscribables.foreach(_.unsafeSubscribe())
      }

      def stop(): Unit = if (isActive) {
        isActive = false
        nativeModifiers.subscribables.foreach(_.unsafeUnsubscribe())
      }

      // hooks for subscribing and unsubscribing the streamable content
      val prependModifiers = js.Array[StaticVMod](DomMountHook(_ => start()), DomUnmountHook(_ => stop()))

      // create the proxy from the modifiers
      val separatedModifiers =
        SeparatedModifiers.from(nativeModifiers.modifiers, config, prependModifiers = prependModifiers)
      createProxy(separatedModifiers, node.nodeType, vNodeId, vNodeNS)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy