Skip to content

Commit

Permalink
New: Add Slot, InsertHooks, and custom input controller configs for w…
Browse files Browse the repository at this point in the history
…eb components
  • Loading branch information
raquo committed Dec 4, 2023
1 parent 5e455db commit de9da36
Show file tree
Hide file tree
Showing 29 changed files with 553 additions and 156 deletions.
2 changes: 0 additions & 2 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
logLevel := Level.Warn

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0")

addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.21.1")
Expand Down
62 changes: 31 additions & 31 deletions src/main/scala/com/raquo/laminar/api/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.raquo.ew
import com.raquo.ew.ewArray
import com.raquo.laminar.api.Implicits.RichSource
import com.raquo.laminar.api.StyleUnitsApi.StyleEncoder
import com.raquo.laminar.inserters.{Inserter, StaticChildInserter, StaticChildrenInserter, StaticTextInserter}
import com.raquo.laminar.inserters.{StaticChildInserter, StaticChildrenInserter, StaticInserter, StaticTextInserter}
import com.raquo.laminar.keys.CompositeKey.CompositeValueMappers
import com.raquo.laminar.keys.{DerivedStyleProp, EventProcessor, EventProp}
import com.raquo.laminar.modifiers.{Binder, Modifier, RenderableNode, RenderableText, Setter}
Expand Down Expand Up @@ -211,72 +211,72 @@ object Implicits {

// -- Methods to convert individual values / nodes / components to inserters --

implicit def textToInserter[A](value: A)(implicit r: RenderableText[A]): Inserter = {
implicit def textToInserter[A](value: A)(implicit r: RenderableText[A]): StaticInserter = {
if (r == RenderableText.textNodeRenderable) {
new StaticChildInserter(value.asInstanceOf[TextNode])
StaticChildInserter.noHooks(value.asInstanceOf[TextNode])
} else {
new StaticTextInserter(r.asString(value))
}
}

implicit def nodeToInserter(node: ChildNode.Base): Inserter = {
new StaticChildInserter(node)
implicit def nodeToInserter(node: ChildNode.Base): StaticChildInserter = {
StaticChildInserter.noHooks(node)
}

implicit def componentToInserter[Component](component: Component)(implicit r: RenderableNode[Component]): Inserter = {
new StaticChildInserter(r.asNode(component))
implicit def componentToInserter[Component: RenderableNode](component: Component): StaticChildInserter = {
StaticChildInserter.noHooksC(component)
}

// -- Methods to convert collections of nodes to inserters --

implicit def nodeOptionToInserter(maybeNode: Option[ChildNode.Base]): Inserter = {
new StaticChildrenInserter(maybeNode.toSeq)
implicit def nodeOptionToInserter(maybeNode: Option[ChildNode.Base]): StaticChildrenInserter = {
StaticChildrenInserter.noHooks(maybeNode.toSeq)
}

implicit def nodeSeqToInserter(nodes: collection.Seq[ChildNode.Base]): Inserter = {
new StaticChildrenInserter(nodes)
implicit def nodeSeqToInserter(nodes: collection.Seq[ChildNode.Base]): StaticChildrenInserter = {
StaticChildrenInserter.noHooks(nodes)
}

implicit def nodeArrayToInserter(nodes: scala.Array[ChildNode.Base]): Inserter = {
new StaticChildrenInserter(nodes)
implicit def nodeArrayToInserter(nodes: scala.Array[ChildNode.Base]): StaticChildrenInserter = {
StaticChildrenInserter.noHooks(nodes)
}

implicit def nodeJsVectorToInserter[N <: ChildNode.Base](nodes: ew.JsVector[N]): Inserter = {
new StaticChildrenInserter(nodes.toList)
implicit def nodeJsVectorToInserter[N <: ChildNode.Base](nodes: ew.JsVector[N]): StaticChildrenInserter = {
StaticChildrenInserter.noHooks(nodes.toList)
}

implicit def nodeJsArrayToInserter[N <: ChildNode.Base](nodes: ew.JsArray[N]): Inserter = {
new StaticChildrenInserter(nodes.asScalaJs.toList)
implicit def nodeJsArrayToInserter[N <: ChildNode.Base](nodes: ew.JsArray[N]): StaticChildrenInserter = {
StaticChildrenInserter.noHooks(nodes.asScalaJs.toList)
}

implicit def nodeSjsArrayToInserter[N <: ChildNode.Base](nodes: js.Array[N]): Inserter = {
new StaticChildrenInserter(nodes.toList)
implicit def nodeSjsArrayToInserter[N <: ChildNode.Base](nodes: js.Array[N]): StaticChildrenInserter = {
StaticChildrenInserter.noHooks(nodes.toList)
}

// -- Methods to convert collections of components to inserters --

implicit def componentOptionToInserter[Component](maybeComponent: Option[Component])(implicit r: RenderableNode[Component]): Inserter = {
new StaticChildrenInserter(r.asNodeOption(maybeComponent).toList)
implicit def componentOptionToInserter[Component: RenderableNode](maybeComponent: Option[Component]): StaticChildrenInserter = {
StaticChildrenInserter.noHooksC(maybeComponent.toList)
}

implicit def componentSeqToInserter[Component](components: collection.Seq[Component])(implicit r: RenderableNode[Component]): Inserter = {
new StaticChildrenInserter(r.asNodeSeq(components.toList))
implicit def componentSeqToInserter[Component: RenderableNode](components: collection.Seq[Component]): StaticChildrenInserter = {
StaticChildrenInserter.noHooksC(components.toList)
}

implicit def componentArrayToInserter[Component](components: scala.Array[Component])(implicit r: RenderableNode[Component]): Inserter = {
new StaticChildrenInserter(r.asNodeSeq(components.toList))
implicit def componentArrayToInserter[Component: RenderableNode](components: scala.Array[Component]): StaticChildrenInserter = {
StaticChildrenInserter.noHooksC(components.toList)
}

implicit def componentJsVectorToInserter[Component](components: ew.JsVector[Component])(implicit r: RenderableNode[Component]): Inserter = {
new StaticChildrenInserter(r.asNodeSeq(components.toList))
implicit def componentJsVectorToInserter[Component: RenderableNode](components: ew.JsVector[Component]): StaticChildrenInserter = {
StaticChildrenInserter.noHooksC(components.toList)
}

implicit def componentJsArrayToInserter[Component](components: ew.JsArray[Component])(implicit r: RenderableNode[Component]): Inserter = {
new StaticChildrenInserter(r.asNodeSeq(components.asScalaJs.toList))
implicit def componentJsArrayToInserter[Component: RenderableNode](components: ew.JsArray[Component]): StaticChildrenInserter = {
StaticChildrenInserter.noHooksC(components.asScalaJs.toList)
}

implicit def componentSjsArrayToInserter[Component](components: js.Array[Component])(implicit r: RenderableNode[Component]): Inserter = {
new StaticChildrenInserter(r.asNodeSeq(components.toList))
implicit def componentSjsArrayToInserter[Component: RenderableNode](components: js.Array[Component]): StaticChildrenInserter = {
StaticChildrenInserter.noHooksC(components.toList)
}

}
Expand Down
6 changes: 5 additions & 1 deletion src/main/scala/com/raquo/laminar/api/MountHooks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.raquo.laminar.lifecycle.MountContext
import com.raquo.laminar.modifiers.{Binder, Modifier, Setter}
import com.raquo.laminar.nodes.{ReactiveElement, ReactiveHtmlElement}

import scala.scalajs.js


trait MountHooks {

Expand Down Expand Up @@ -55,7 +57,9 @@ trait MountHooks {
var maybeSubscription: Option[DynamicSubscription] = None
// We start the context in loose mode for performance, because it's cheaper to go from there
// to strict mode, than the other way. The inserters are able to handle any initial mode.
val lockedInsertContext = InsertContext.reserveSpotContext(element, strictMode = false)
val lockedInsertContext = InsertContext.reserveSpotContext(
element, strictMode = false, hooks = js.undefined
)
element.amend(
onMountUnmountCallback[El](
mount = { mountContext =>
Expand Down
28 changes: 28 additions & 0 deletions src/main/scala/com/raquo/laminar/inputs/InputController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,34 @@ object InputController {
setDomValue = (el, v) => DomApi.setChecked(el.ref, v)
)

/** Pool of memoized controller configs, to reuse between the various element types */
private val customControllerConfigs: js.Dictionary[InputControllerConfig[dom.html.Element, _]] = js.Dictionary()

/** Controller configs for custom properties. Cached by key for efficiency.
* This method is used to add input controller support to web components.
*/
def customConfig[A](
prop: HtmlProp[A, _],
eventProps: JsArray[EventProp[_]],
initial: A
): InputControllerConfig[dom.html.Element, A] = {
// Note: we need the codec because in principle we might have two different
// prop-s with the same DOM name but different codecs, for example valueStr
// and valueList for space-separated composite list props.
val eventPropsStr = eventProps.reduce((acc: String, p: EventProp[_]) => acc + s"-${p.name}", "")
val key = prop.name + "-" + prop.codec.toString + eventPropsStr
val knownConfig = customControllerConfigs.getOrElseUpdate(key, {
new InputControllerConfig[dom.html.Element, A](
initialValue = initial,
prop = prop,
allowedEventProps = eventProps,
getDomValue = el => DomApi.getHtmlProperty(el, prop).get,
setDomValue = (el, v) => DomApi.setHtmlProperty(el, prop, v)
)
})
knownConfig.asInstanceOf[InputControllerConfig[dom.html.Element, A]]
}

def controlled[Ref <: dom.html.Element, Ev <: dom.Event, A, B](
listener: EventListener[Ev, B],
updater: KeyUpdater[ReactiveHtmlElement[Ref], HtmlProp[A, _], A]
Expand Down
17 changes: 11 additions & 6 deletions src/main/scala/com/raquo/laminar/inserters/ChildInserter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ object ChildInserter {

def apply[Component] (
childSource: Observable[Component],
renderable: RenderableNode[Component]
renderable: RenderableNode[Component],
hooks: js.UndefOr[InserterHooks]
): DynamicInserter = {
new DynamicInserter(
preferStrictMode = true,
Expand All @@ -22,17 +23,19 @@ object ChildInserter {
var maybeLastSeenChild: js.UndefOr[ChildNode.Base] = js.undefined
childSource.foreach { newComponent =>
val newChildNode = renderable.asNode(newComponent)
switchToChild(maybeLastSeenChild, newChildNode, ctx)
switchToChild(maybeLastSeenChild, newChildNode, ctx, hooks)
maybeLastSeenChild = newChildNode
}(owner)
}
},
hooks = hooks
)
}

def switchToChild(
maybeLastSeenChild: js.UndefOr[ChildNode.Base],
newChildNode: ChildNode.Base,
ctx: InsertContext
ctx: InsertContext,
hooks: js.UndefOr[InserterHooks]
): Unit = {
if (!ctx.strictMode) {
// #Note: previously in ChildInserter we only did this once in insertFn.
Expand All @@ -49,7 +52,8 @@ object ChildInserter {
ParentNode.insertChildAfter(
parent = ctx.parentNode,
newChild = newChildNode,
referenceChild = ctx.sentinelNode
referenceChild = ctx.sentinelNode,
hooks = hooks
)
()
} { lastSeenChild =>
Expand All @@ -60,7 +64,8 @@ object ChildInserter {
val replaced = ParentNode.replaceChild(
parent = ctx.parentNode,
oldChild = lastSeenChild,
newChild = newChildNode
newChild = newChildNode,
hooks = hooks
)
if (replaced || (lastSeenChild eq newChildNode)) { // #TODO[Performance,Integrity] Not liking this redundant auto-distinction
// The only time we DON'T decrement this is when replacing fails for unexpected reasons.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@ object ChildTextInserter {
textNode.ref.textContent = renderable.asString(newValue)
}
}(owner)
}
},
hooks = js.undefined
)
}

def switchToText(newTextNode: TextNode, ctx: InsertContext): Unit = {
// First event: inserting the child for the first time: replace sentinel comment node with new TextNode
ParentNode.replaceChild(parent = ctx.parentNode, oldChild = ctx.sentinelNode, newChild = newTextNode)
ParentNode.replaceChild(
parent = ctx.parentNode,
oldChild = ctx.sentinelNode,
newChild = newTextNode,
hooks = js.undefined
)

ctx.sentinelNode = newTextNode
if (ctx.strictMode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.raquo.laminar.DomApi
import com.raquo.laminar.modifiers.RenderableNode
import com.raquo.laminar.nodes.{ChildNode, ParentNode, ReactiveElement}

import scala.scalajs.js

/** Note: this is a low level inserter. It is the fastest one in certain cases,
* but due to its rather imperative API, its usefulness is very limited.
*
Expand All @@ -21,7 +23,8 @@ object ChildrenCommandInserter {

def apply[Component] (
commands: EventStream[CollectionCommand[Component]],
renderableNode: RenderableNode[Component]
renderableNode: RenderableNode[Component],
hooks: js.UndefOr[InserterHooks]
): DynamicInserter = {
new DynamicInserter(
preferStrictMode = true,
Expand All @@ -32,11 +35,13 @@ object ChildrenCommandInserter {
parentNode = ctx.parentNode,
sentinelNode = ctx.sentinelNode,
ctx.extraNodeCount,
renderableNode
renderableNode,
hooks
)
ctx.extraNodeCount += nodeCountDiff
}(owner)
}
},
hooks = hooks
)
}

Expand All @@ -45,7 +50,8 @@ object ChildrenCommandInserter {
parentNode: ReactiveElement.Base,
sentinelNode: ChildNode.Base,
extraNodeCount: Int,
renderableNode: RenderableNode[Component]
renderableNode: RenderableNode[Component],
hooks: js.UndefOr[InserterHooks]
): Int = {
var nodeCountDiff = 0
def findSentinelIndex(): Int = DomApi.indexOfChild(
Expand All @@ -59,7 +65,9 @@ object ChildrenCommandInserter {
val inserted = ParentNode.insertChildAtIndex(
parent = parentNode,
child = renderableNode.asNode(node),
index = findSentinelIndex() + extraNodeCount + 1)
index = findSentinelIndex() + extraNodeCount + 1,
hooks
)
if (inserted) {
nodeCountDiff = 1
}
Expand All @@ -69,7 +77,8 @@ object ChildrenCommandInserter {
val inserted = ParentNode.insertChildAfter(
parent = parentNode,
newChild = renderableNode.asNode(node),
referenceChild = sentinelNode
referenceChild = sentinelNode,
hooks
)
if (inserted) {
nodeCountDiff = 1
Expand All @@ -79,7 +88,8 @@ object ChildrenCommandInserter {
val inserted = ParentNode.insertChildAtIndex(
parent = parentNode,
child = renderableNode.asNode(node),
index = findSentinelIndex() + atIndex + 1
index = findSentinelIndex() + atIndex + 1,
hooks
)
if (inserted) {
nodeCountDiff = 1
Expand All @@ -98,7 +108,8 @@ object ChildrenCommandInserter {
ParentNode.replaceChild(
parent = parentNode,
oldChild = renderableNode.asNode(node),
newChild = renderableNode.asNode(withNode)
newChild = renderableNode.asNode(withNode),
hooks
)
}

Expand Down
Loading

0 comments on commit de9da36

Please sign in to comment.