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

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

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

import com.raquo.airstream.ownership.{DynamicSubscription, Owner, Subscription}
import com.raquo.laminar.modifiers.Modifier
import com.raquo.laminar.nodes.ReactiveElement

import scala.scalajs.js

/** Inserter is a class that can insert child nodes into [[InsertContext]].
  *
  * This is needed in `onMountInsert`, or when rendering dynamic children
  * with `child <-- ...`, `children <-- ...`, etc.
  *
  * If you don't have a [[InsertContext]], you can render the Inserter
  * just by calling its `apply` method. It will work the same way as
  * rendering any other child node, static or dynamic, would.
  *
  * So, you can use [[Inserter]] essentially as (an implicit-powered)
  * sypertype of regular laminar elements and dynamic inserters like
  * `children <-- ...`. We use it this way in `onMountInsert`, for example.
  */
sealed trait Inserter extends Modifier[ReactiveElement.Base]

trait Hookable[+Self <: Inserter] { this: Inserter =>

  /** Create a copy of the inserter that will apply these
    * additional hooks after the original inserter's hooks.
    */
  def withHooks(addHooks: InserterHooks): Self
}

trait StaticInserter extends Inserter {

  def renderInContext(ctx: InsertContext): Unit
}

// @TODO[API] Inserter really wants to extend Binder. And yet.

/** Inserter is a modifier that lets you insert child node(s) on mount.
  * When used with onMountInsert, it "immediately" reserves an insertion
  * spot and then on every mount it inserts the node(s) into the same spot.
  *
  * Note: As a Modifier this is not idempotent, but overall
  * it behaves as you would expect. See docs for more details.
  *
  * Note: If you DO provide initialContext, its parentNode MUST always
  * be the same `element` that you apply this Modifier to.
  */
class DynamicInserter(
  initialContext: Option[InsertContext] = None,
  preferStrictMode: Boolean,
  insertFn: (InsertContext, Owner, js.UndefOr[InserterHooks]) => Subscription,
  hooks: js.UndefOr[InserterHooks] = js.undefined
) extends Inserter with Hookable[DynamicInserter] {

  def bind(element: ReactiveElement.Base): DynamicSubscription = {
    // @TODO[Performance] The way it's used in `onMountInsert`, we create a DynSub inside DynSub.
    //  - Currently this does not seem avoidable as we don't want to expose a `map` on DynSub
    //  - That would allow you to create leaky resources without having a reference to the owner
    //  - But maybe we require the user to provide proof of owner: dynSub.map(project)(owner) that must match DynSub
    // #Note we want to remember this context even after subscription is deactivated.
    //  Yes, we expect the subscription to re-activate with this initial state
    //  because it would match the state of the DOM upon reactivation
    //  (unless some of the managed child elements were externally removed from the DOM,
    //  which Laminar should be able to recover from).
    val insertContext = initialContext.getOrElse(
      InsertContext.reserveSpotContext(element, strictMode = preferStrictMode, hooks)
    )

    ReactiveElement.bindSubscriptionUnsafe(element) { mountContext =>
      insertFn(insertContext, mountContext.owner, hooks)
    }
  }

  override def apply(element: ReactiveElement.Base): Unit = {
    bind(element)
  }

  override def withHooks(addHooks: InserterHooks): DynamicInserter = {
    val newHooks = addHooks.appendTo(hooks)
    new DynamicInserter(initialContext, preferStrictMode, insertFn, newHooks)
  }

  /** Call this to get a copy of Inserter with a context locked to a certain element.
    * We use this to "reserve a spot" for future nodes when a bind(c => inserter) modifier
    * is initialized, as opposed to waiting until subscription is activated.
    *
    * The arrangement is admittedly a bit weird, but is required to build a smooth end user API.
    */
  def withContext(context: InsertContext): DynamicInserter = {
    // Note: preferStrictMode has no effect here, because initial context is defined.
    new DynamicInserter(Some(context), preferStrictMode = false, insertFn, hooks)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy