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

com.raquo.laminar.nodes.ReactiveElement.scala Maven / Gradle / Ivy

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

import com.raquo.airstream.core.{Observable, Observer, Subscription}
import com.raquo.airstream.eventbus.{EventBus, EventBusSource, WriteBus}
import com.raquo.airstream.eventstream.EventStream
import com.raquo.airstream.ownership.{Owned, Owner}
import com.raquo.airstream.signal.Signal
import com.raquo.dombuilder.generic
import com.raquo.dombuilder.jsdom.JsCallback
import com.raquo.domtypes.generic.keys.EventProp
import com.raquo.laminar.emitter.EventPropEmitter
import com.raquo.laminar.lifecycle.{MountEvent, NodeDidMount, NodeWasDiscarded, NodeWillUnmount, ParentChangeEvent}
import com.raquo.laminar.nodes.ReactiveChildNode.isParentMounted
import com.raquo.laminar.nodes.ReactiveElement.noMountEvents
import com.raquo.laminar.receivers.{ChildReceiver, ChildrenCommandReceiver, ChildrenReceiver, MaybeChildReceiver, TextChildReceiver}
import org.scalajs.dom

trait ReactiveElement[+Ref <: dom.Element]
  extends ReactiveNode
  with ReactiveChildNode[Ref]
  with generic.nodes.Element[ReactiveNode, Ref, dom.Node]
  with generic.nodes.EventfulNode[ReactiveNode, Ref, dom.Element, dom.Node, dom.Event, JsCallback]
  with generic.nodes.ParentNode[ReactiveNode, Ref, dom.Node]
  with Owner {

  /** Event bus that emits parent change events.
    * For efficiency, it is only populated when someone accesses [[parentChangeEvents]]
    */
  private[laminar] var maybeParentChangeBus: Option[EventBus[ParentChangeEvent]] = None

  /** Event bus that emits this node's mount events.
    * For efficiency, it is only populated when someone accesses [[thisNodeMountEvents]]
    */
  private[laminar] var maybeThisNodeMountEventBus: Option[EventBus[MountEvent]] = None

  /** Stream of parent change events for this node.
    * For efficiency, it is lazy loaded, only being initialized when accessed,
    * either directly or (more commonly) as a dependency of [[mountEvents]]
    */
  lazy val parentChangeEvents: EventStream[ParentChangeEvent] = {
    val parentChangeBus = new EventBus[ParentChangeEvent]
    maybeParentChangeBus = Some(parentChangeBus)
    parentChangeBus.events
  }

  lazy val maybeParentSignal: Signal[Option[BaseParentNode]] = parentChangeEvents
    .filter(_.alreadyChanged) // @TODO[Integrity] is this important?
    .map(_.maybeNextParent)
    .toSignal(maybeParent)

  /** Emits mount events that were caused by this element's parent changing its parent,
    * or any such changes up the chain. Does not include mount events triggered by this
    * element changing its parent - see [[thisNodeMountEvents]] for that. */
  private lazy val ancestorMountEvents: EventStream[MountEvent] = {
    maybeParentSignal.map {
      case Some(nextParent: ReactiveElement[_]) =>
        nextParent.mountEvents
      case _ =>
        noMountEvents
    }.flatten
  }

  /** Emits mount events caused by this node changing its parent. Does not include mount
    * events triggered by changes higher in the hierarchy (grandparent and up) – see
    * [[ancestorMountEvents]] for that. */
  private lazy val thisNodeMountEvents: EventStream[MountEvent] = {
    val thisNodeMountEventBus = new EventBus[MountEvent]
    maybeThisNodeMountEventBus = Some(thisNodeMountEventBus)
    thisNodeMountEventBus.events
  }

  /** Emits mount events for this node, including mount events fired by all of its ancestors */
  lazy val mountEvents: EventStream[MountEvent] = {
    EventStream.merge(ancestorMountEvents, thisNodeMountEvents)
  }

  /** Create and get a stream of events on this node */
  def events[Ev <: dom.Event](
    eventProp: EventProp[Ev],
    useCapture: Boolean = false,
    stopPropagation: Boolean = false, // @TODO[API] This is inconsistent with EventPropEmitter API. Fix or ok?
    preventDefault: Boolean = false // @TODO[API] This is inconsistent with EventPropEmitter API. Fix or ok?
  ): EventStream[Ev] = {
    val eventBus = new EventBus[Ev]
    val setter = new EventPropEmitter[Ev, Ev, this.type](
      eventBus.writer,
      eventProp,
      useCapture,
      processor = Some(_)
    )
    setter(this)
    eventBus.events
  }

  final def <--[V](childReceiver: ChildReceiver.type): ChildReceiver = new ChildReceiver(this)

  final def <--[V](maybeChildReceiver: MaybeChildReceiver.type): MaybeChildReceiver = new MaybeChildReceiver(this)

  final def <--[V](textChildReceiver: TextChildReceiver.type): TextChildReceiver = new TextChildReceiver(this)

  final def <--[V](childrenReceiver: ChildrenReceiver.type): ChildrenReceiver = new ChildrenReceiver(this)

  final def <--[V](childrenCommandReceiver: ChildrenCommandReceiver.type): ChildrenCommandReceiver = new ChildrenCommandReceiver(this)

  // @TODO[Naming] Not a fan of `subscribeO` name, but it needs to be different from `subscribe` for type inference to work

  def subscribeO[V](observable: Observable[V])(observer: Observer[V]): Subscription = {
    observable.addObserver(observer)(owner = this)
  }

  // @TODO[Naming] Rename to subscribeThisO?
  def subscribeO[V](getObservable: this.type => Observable[V])(observer: Observer[V]): Subscription = {
    subscribeO(getObservable(this))(observer)
  }

  def subscribe[V](observable: Observable[V])(onNext: V => Unit): Subscription = {
    subscribeO(observable)(Observer(onNext))
  }

  // @TODO[Naming] Rename to subscribeThis?
  def subscribe[V](getObservable: this.type => Observable[V])(onNext: V => Unit): Subscription = {
    subscribeO(getObservable(this))(Observer(onNext))
  }

  def subscribeBus[V](
    sourceStream: EventStream[V],
    targetBus: WriteBus[V]
  ): EventBusSource[V] = {
    targetBus.addSource(sourceStream)(owner = this)
  }

  /** Check whether the node is currently mounted.
    *
    * You can use this method to simplify your code and possibly improve performance
    * where you'd otherwise need to subscribe to and transform [[mountEvents]]
    */
  def isMounted: Boolean = {
    isParentMounted(maybeParent)
  }

  /** This hook is exposed by Scala DOM Builder for this exact purpose */
  override def willSetParent(maybeNextParent: Option[BaseParentNode]): Unit = {
    // @TODO[Integrity] In dombuilder, willSetParent is called at incorrect time in ParentNode.appendChild
    // dom.console.log(">>>> willSetParent", this.ref.tagName + "(" + this.ref.textContent + ")", maybeParent == maybeNextParent, maybeParent.toString, maybeNextParent.toString)
    if (maybeNextParent != maybeParent) {
      maybeParentChangeBus.foreach(bus => {
        bus.writer.onNext(ParentChangeEvent(
          alreadyChanged = false,
          maybePrevParent = maybeParent,
          maybeNextParent = maybeNextParent
        ))
      })
      if (!isParentMounted(maybeNextParent) && isParentMounted(maybeParent)) {
        maybeThisNodeMountEventBus.foreach { bus =>
          bus.writer.onNext(NodeWillUnmount)
        }
      }
    }
  }

  override def setParent(maybeNextParent: Option[BaseParentNode]): Unit = {
    // @TODO[Integrity] Beware of calling setParent directly – willSetParent will not be called?
    // @TODO[Integrity] this method (and others) should be protected
    val maybePrevParent = maybeParent
    super.setParent(maybeNextParent)
    // dom.console.log(">>>> setParent", this.ref.tagName + "(" + this.ref.textContent + ")", maybePrevParent == maybeNextParent, maybePrevParent.toString, maybeNextParent.toString)
    if (maybeNextParent != maybePrevParent) {
      maybeParentChangeBus.foreach( bus => {
        bus.writer.onNext(ParentChangeEvent(
          alreadyChanged = true,
          maybePrevParent = maybePrevParent,
          maybeNextParent = maybeNextParent
        ))
      })
      val prevParentIsMounted = isParentMounted(maybePrevParent)
      val nextParentIsMounted = isParentMounted(maybeNextParent)
      if (!prevParentIsMounted && nextParentIsMounted) {
        maybeThisNodeMountEventBus.foreach(_.writer.onNext(NodeDidMount))
      }
      if (prevParentIsMounted && !nextParentIsMounted) {
        maybeThisNodeMountEventBus.foreach { bus =>
          bus.writer.onNext(NodeWasDiscarded) // @TODO this should be optional
        }
      }
    }
  }

  override protected[this] def onOwned(owned: Owned): Unit = {
    val isFirstSubscription = possessions.length == 1
    if (isFirstSubscription) {
      // Create a subscription that will kill all subscriptions that this element owns when it is discarded.
      // Note: Once this subscription is created, it will persist until this element is discarded.
      //       This might be suboptimal in a narrow range of conditions, I don't think anyone will run into those.
      // @TODO[Performance] ^^^^ The above can be addressed by e.g. overriding onKilledExternally (but permissions)
      mountEvents.filter(_ == NodeWasDiscarded).foreach(_ => killPossessions() )(owner = this)
    }
  }

}

object ReactiveElement {

  private val noMountEvents: EventStream[MountEvent] = EventStream.fromSeq(Nil, emitOnce = true)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy