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

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

The newest version!
package outwatch.interpreter

import org.scalajs.dom
import outwatch._
import outwatch.helpers.MutableNestedArray
import outwatch.helpers.NativeHelpers._
import colibri._
import snabbdom.{DataObject, Hooks, VNodeProxy}

import scala.scalajs.js

// This file is about interpreting VDomModifiers for building snabbdom virtual nodes.
// We want to convert a div(modifiers) into a VNodeProxy that we can use for patching
// with snabbdom.
//
// This code is really performance cirtical, because every patch call is preceeded by
// interpreting all VDomModifiers involed. This code is written in a mutable fashion
// using native js types to reduce overhead.

// This represents the structured definition of a VNodeProxy (like snabbdom expects it).
private[outwatch] class SeparatedModifiers {
  var hasOnlyTextChildren = true
  var nextModifiers: js.UndefOr[js.Array[StaticVDomModifier]] = js.undefined
  var proxies: js.UndefOr[js.Array[VNodeProxy]] = js.undefined
  var attrs: js.UndefOr[js.Dictionary[DataObject.AttrValue]] = js.undefined
  var props: js.UndefOr[js.Dictionary[DataObject.PropValue]] = js.undefined
  var styles: js.UndefOr[js.Dictionary[DataObject.StyleValue]] = js.undefined
  var emitters: js.UndefOr[js.Dictionary[js.Function1[dom.Event, Unit]]] = js.undefined
  var keyOption: js.UndefOr[Key.Value] = js.undefined
  var initHook: js.UndefOr[Hooks.HookSingleFn] = js.undefined
  var insertHook: js.UndefOr[Hooks.HookSingleFn] = js.undefined
  var prePatchHook: js.UndefOr[Hooks.HookPairFn] = js.undefined
  var updateHook: js.UndefOr[Hooks.HookPairFn] = js.undefined
  var postPatchHook: js.UndefOr[Hooks.HookPairFn] = js.undefined
  var destroyHook: js.UndefOr[Hooks.HookSingleFn] = js.undefined
  var domUnmountHook: js.UndefOr[Hooks.HookSingleFn] = js.undefined
}

private[outwatch] object SeparatedModifiers {
  def from(modifiers: MutableNestedArray[StaticVDomModifier], prependModifiers: js.UndefOr[js.Array[StaticVDomModifier]] = js.undefined, appendModifiers: js.UndefOr[js.Array[StaticVDomModifier]] = js.undefined): SeparatedModifiers = {
    val separatedModifiers = new SeparatedModifiers
    import separatedModifiers._

    @inline def assureProxies() = proxies getOrElse assign(new js.Array[VNodeProxy])(proxies = _)
    @inline def assureNextModifiers() = nextModifiers getOrElse assign(new js.Array[StaticVDomModifier])(nextModifiers = _)
    @inline def assureEmitters() = emitters getOrElse assign(js.Dictionary[js.Function1[dom.Event, Unit]]())(emitters = _)
    @inline def assureAttrs() = attrs getOrElse assign(js.Dictionary[DataObject.AttrValue]())(attrs = _)
    @inline def assureProps() = props getOrElse assign(js.Dictionary[DataObject.PropValue]())(props = _)
    @inline def assureStyles() = styles getOrElse assign(js.Dictionary[DataObject.StyleValue]())(styles = _)
    @inline def setSpecialStyle(styleName: String)(title: String, value: String): Unit = {
      val styles = assureStyles()
      styles.raw(styleName).fold {
        styles(styleName) = js.Dictionary[String](title -> value): DataObject.StyleValue
      } { style =>
        style.asInstanceOf[js.Dictionary[String]](title) = value
      }
    }
    @inline def createHooksSingle[T](current: js.UndefOr[js.Function1[T, Unit]], hook: js.Function1[T, Unit]): js.Function1[T, Unit] =
      current.fold(hook)(current => { p => current(p); hook(p) })
    @inline def createHooksPair[T](current: js.UndefOr[js.Function2[T, T, Unit]], hook: js.Function2[T, T, Unit]): js.Function2[T, T, Unit] =
      current.fold(hook)(current => { (o,p) => current(o, p); hook(o, p) })


    // append unmount hook for when patching a different proxy out of the dom.
    // the proxies will then have different OutwatchStates and we then need to
    // call the unmount hook of the oldProxy.
    postPatchHook = { (oldProxy, proxy) =>
      if (!NativeModifiers.equalsVNodeIds(proxy._id, oldProxy._id)) {
        oldProxy._unmount.foreach(_(oldProxy))
      }
    }: Hooks.HookPairFn

    def append(mod: StaticVDomModifier): Unit = mod match {
      case VNodeProxyNode(proxy) =>
        hasOnlyTextChildren = hasOnlyTextChildren && proxy.data.isEmpty && proxy.text.isDefined
        val proxies = assureProxies()
        proxies += proxy
        ()
      case a : BasicAttr =>
        val attrs = assureAttrs()
        attrs(a.title) = a.value
        ()
      case a : AccumAttr =>
        val attrs = assureAttrs()
        val attr = attrs.raw(a.title)
        attr.fold {
          attrs(a.title) = a.value
        } { attr =>
          attrs(a.title) = a.accum(attr, a.value)
        }
        ()
      case p : Prop =>
        val props = assureProps()
        props(p.title) = p.value
        ()
      case s: BasicStyle =>
        val styles = assureStyles()
        styles(s.title) = s.value
        ()
      case s: DelayedStyle =>
        setSpecialStyle(StyleKey.delayed)(s.title, s.value)
        ()
      case s: RemoveStyle =>
        setSpecialStyle(StyleKey.remove)(s.title, s.value)
        ()
      case s: DestroyStyle =>
        setSpecialStyle(StyleKey.destroy)(s.title, s.value)
        ()
      case a: AccumStyle =>
        val styles = assureStyles()
        val style = styles.raw(a.title)
        style.fold {
          styles(a.title) = a.value
        } { style =>
          styles(a.title) = a.accum(style.asInstanceOf[String], a.value): DataObject.StyleValue
        }
        ()
      case k: Key =>
        keyOption = k.value
        ()
      case e: Emitter =>
        val emitters = assureEmitters()
        val emitter = emitters.raw(e.eventType)
        emitters(e.eventType) = createHooksSingle(emitter, e.trigger)
        ()
      case h: DomMountHook =>
        insertHook = createHooksSingle(insertHook, h.trigger)
        postPatchHook = createHooksPair[VNodeProxy](postPatchHook, { (oldproxy, proxy) =>
          if (!NativeModifiers.equalsVNodeIds(oldproxy._id, proxy._id)) {
            h.trigger(proxy)
          }
        })
        ()
      case h: DomUnmountHook =>
        destroyHook = createHooksSingle(destroyHook, h.trigger)
        domUnmountHook = createHooksSingle(domUnmountHook, h.trigger)
        ()
      case h: DomUpdateHook =>
        postPatchHook = createHooksPair[VNodeProxy](postPatchHook, { (oldproxy, proxy) =>
          if (NativeModifiers.equalsVNodeIds(oldproxy._id, proxy._id)) {
            h.trigger(proxy, proxy)
          }
        })
        ()
      case h: DomPreUpdateHook =>
        prePatchHook = createHooksPair[VNodeProxy](prePatchHook, { (oldproxy, proxy) =>
          if (NativeModifiers.equalsVNodeIds(oldproxy._id, proxy._id)) {
            h.trigger(oldproxy, proxy)
          }
        })
        ()
      case h: InitHook =>
        initHook = createHooksSingle(initHook, h.trigger)
        ()
      case h: InsertHook =>
        insertHook = createHooksSingle(insertHook, h.trigger)
        ()
      case h: PrePatchHook =>
        prePatchHook = createHooksPair(prePatchHook, h.trigger)
        ()
      case h: UpdateHook =>
        updateHook = createHooksPair(updateHook, h.trigger)
        ()
      case h: PostPatchHook =>
        postPatchHook = createHooksPair(postPatchHook, h.trigger)
        ()
      case h: DestroyHook =>
        destroyHook = createHooksSingle(destroyHook, h.trigger)
        ()
      case n: NextVDomModifier =>
        val nextModifiers = assureNextModifiers()
        nextModifiers += n.modifier
        ()
    }

    prependModifiers.foreach(_.foreach(append))
    modifiers.foreach(append)
    appendModifiers.foreach(_.foreach(append))

    separatedModifiers
  }
}

// Each VNode or each streamed CompositeVDomModifier contains static and
// potentially dynamic content (i.e. streams). The contained VDomModifiers
// within this VNode or this CompositeVDomModifier need to be transformed into
// a list of static VDomModifiers (non-dynamic like attributes, vnode proxies,
// ... that can directly be rendered) and a combined observable of all dynamic
// content (like StreamModifier).
//
// The NativeModifier class represents exactly that: the static and dynamic
// part of a list of VDomModifiers. The static part is an array of all
// modifiers that are to-be-rendered at the current point in time. The dynamic
// part is an observable that changes the previously mentioned array to reflect
// the new state.
//
// Example
//  Input:
//    div (
//     "a",
//     observable
//     observable2
//    )
//  Output:
//    - currently active modifiers: Array("a", ?, ?)
//    - dynamic changes: collections of callbacks that fill the array of active modifiers

private[outwatch] class NativeModifiers(
  val modifiers: MutableNestedArray[StaticVDomModifier],
  val subscribables: MutableNestedArray[Subscribable],
  val hasStream: Boolean
)
private[outwatch] class Subscribable(
  newCancelable: Observer[Unit] => Cancelable
) {
  var subscription: Cancelable = null

  def subscribe(sink: Observer[Unit]): Unit = if (subscription == null) {
    // this is a weird function, it ignores a subsription, eventhough it does not know
    // wether this specific observer is already subscribed. In this case it is okay,
    // because this is an internal class that only ever is called with the same observer
    val variable = Cancelable.variable()
    subscription = variable
    variable() = newCancelable(sink)
  }

  def unsubscribe(): Unit = if (subscription != null) {
    subscription.cancel()
    subscription = null
  }
}

private[outwatch] object NativeModifiers {
  def from(appendModifiers: js.Array[_ <: VDomModifier], config: RenderConfig): NativeModifiers = {
    val allModifiers = new MutableNestedArray[StaticVDomModifier]()
    val allSubscribables = new MutableNestedArray[Subscribable]()
    var hasStream = false

    def append(subscribables: MutableNestedArray[Subscribable], modifiers: MutableNestedArray[StaticVDomModifier], modifier: VDomModifier, inStream: Boolean): Unit = {

      @inline def appendStatic(mod: StaticVDomModifier): Unit = {
        modifiers.push(mod)
        ()
      }

      @inline def appendStream(mod: StreamModifier): Unit = {
        hasStream = true

        val streamedModifiers = new MutableNestedArray[StaticVDomModifier]()
        val streamedSubscribables = new MutableNestedArray[Subscribable]()

        def handleModifier(modifier: VDomModifier) = {
          streamedSubscribables.foreach(_.unsubscribe())
          streamedSubscribables.clear()
          streamedModifiers.clear()
          append(streamedSubscribables, streamedModifiers, modifier, inStream = true)
        }

        subscribables.push(new Subscribable(
          sink => mod.subscription(Observer.create(
            { modifier =>
              handleModifier(modifier)
              sink.onNext(())
            },
            { error =>
              handleModifier(config.errorModifier(error))
              sink.onNext(())
              sink.onError(error)
            }
          ))
        ))

        modifiers.push(streamedModifiers)
        subscribables.push(streamedSubscribables)
        ()
      }

      modifier match {
        case EmptyModifier => ()
        case c: CompositeModifier => c.modifiers.foreach(append(subscribables, modifiers, _, inStream))
        case h: DomHook if inStream => mirrorStreamedDomHook(h).foreach(appendStatic)
        case mod: StaticVDomModifier => appendStatic(mod)
        case child: VNode  => appendStatic(VNodeProxyNode(SnabbdomOps.toSnabbdom(child, config)))
        case child: StringVNode  => appendStatic(VNodeProxyNode(VNodeProxy.fromString(child.text)))
        case m: StreamModifier => appendStream(m)
        case s: CancelableModifier => subscribables.push(new Subscribable(_ => s.subscription()))
        case m: SyncEffectModifier => append(subscribables, modifiers, m.unsafeRun(), inStream)
        case m: ChildCommandsModifier => append(subscribables, modifiers, ChildCommand.stream(m.commands, config), inStream)
      }
    }

    appendModifiers.foreach(append(allSubscribables, allModifiers, _, inStream = false))

    new NativeModifiers(allModifiers, allSubscribables, hasStream)
  }

  // if a dom mount hook is streamed, we want to emulate an intuitive interface as if they were static.
  // This means we need to translate the next update to a mount event and an unmount event for the previously
  // streamed hooks. the first update event needs to be ignored to emulate static update events.
  private def mirrorStreamedDomHook(h: DomHook): js.Array[StaticVDomModifier] = h match {
    case h: DomMountHook =>
      // trigger once for the next update event and always for each mount event.
      // if we are streamed in with an insert event, then ignore all update events.
      var triggered = false
      js.Array(
        InsertHook { p =>
          triggered = true
          h.trigger(p)
        },
        PostPatchHook { (o, p) =>
          if (!triggered || !equalsVNodeIds(o._id, p._id)) h.trigger(p)
          triggered = true
        })
    case h: DomPreUpdateHook =>
      // ignore the next pre-update event, we are streamed into the node with this update
      // trigger on all succeeding pre-update events. if we are streamed in with an insert
      // event, then trigger on next update events as well.
      var triggered = false
      js.Array(
        InsertHook { _ =>
          triggered = true
        },
        PrePatchHook { (o, p) =>
          if (triggered && equalsVNodeIds(o._id, p._id)) h.trigger(o, p)
          triggered = true
        }
      )
    case h: DomUpdateHook =>
      // ignore the next update event, we are streamed into the node with this update
      // trigger on all succeeding update events. if we are streamed in with an insert
      // event, then trigger on next update events as well.
      var triggered = false
      js.Array(
        InsertHook { _ =>
          triggered = true
        },
        PostPatchHook { (o, p) =>
          if (triggered && equalsVNodeIds(o._id, p._id)) h.trigger(o, p)
          triggered = true
        }
      )
    case h: DomUnmountHook =>
      // we call the unmount hook, whenever this hook is freshly superseded by a new modifier
      // in a stream. whenever the node is patched afterwards we check whether we are still
      // present in the node. if not, we are unmounted and call the hook. We additionally
      // react to the normal unmount event.
      var triggered = false
      var isOpen = true
      js.Array(
        h,
        InsertHook { _ => triggered = true },
        UpdateHook { (o, p) =>
          if (triggered && equalsVNodeIds(o._id, p._id)) isOpen = false
          triggered = true
        },
        NextVDomModifier(UpdateHook { (o, p) =>
          if (isOpen && equalsVNodeIds(o._id, p._id)) h.trigger(p)
          isOpen = true
        })
      )
  }

  @inline def equalsVNodeIds(oid: js.UndefOr[Int], nid: js.UndefOr[Int]): Boolean = {
    // Just doing oid == nid in scala does boxes-runtime equals
    oid.fold(nid.isEmpty)(oid => nid.fold(false)(_ == oid))
  }
}

private object StyleKey {
  @inline def delayed = "delayed"
  @inline def remove = "remove"
  @inline def destroy = "destroy"
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy