Skip to content

Commit

Permalink
New: RenderableSeq instances for Option and js.UndefOr
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Aug 15, 2024
1 parent 71026d6 commit 90d238c
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 54 deletions.
48 changes: 23 additions & 25 deletions src/main/scala/com/raquo/laminar/api/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ trait Implicits extends Implicits.LowPriorityImplicits with CompositeValueMapper
// -- Methods to convert collections of Setter[El] to a single Setter[El] --

/** Create a [[Setter]] that applies the optionally provided [[Setter]], or else does nothing. */
implicit def optionToSetter[El <: ReactiveElement.Base](maybeSetter: Option[Setter[El]]): Setter[El] = {
Setter(element => maybeSetter.foreach(_.apply(element)))
}
// implicit def optionToSetter[El <: ReactiveElement.Base](maybeSetter: Option[Setter[El]]): Setter[El] = {
// Setter(element => maybeSetter.foreach(_.apply(element)))
// }

/** Combine a js.Array of [[Setter]]-s into a single [[Setter]] that applies them all. */
implicit def seqToSetter[Collection[_], El <: ReactiveElement.Base](
Expand All @@ -71,8 +71,7 @@ trait Implicits extends Implicits.LowPriorityImplicits with CompositeValueMapper
implicit renderableSeq: RenderableSeq[Collection]
): Setter[El] = {
Setter { element =>
val settersSeq = renderableSeq.toSeq(setters)
settersSeq.foreach(_.apply(element))
renderableSeq.foreach(setters)(_.apply(element))
}
}

Expand All @@ -86,13 +85,13 @@ trait Implicits extends Implicits.LowPriorityImplicits with CompositeValueMapper
// -- Methods to convert collections of Modifier[El]-like things to Modifier[El] --

/** Create a modifier that applies an optional modifier, or does nothing if option is empty */
implicit def optionToModifier[A, El <: ReactiveElement.Base](
maybeModifier: Option[A]
)(
implicit asModifier: A => Modifier[El]
): Modifier[El] = {
Modifier(element => maybeModifier.foreach(asModifier(_).apply(element)))
}
// implicit def optionToModifier[A, El <: ReactiveElement.Base](
// maybeModifier: Option[A]
// )(
// implicit asModifier: A => Modifier[El]
// ): Modifier[El] = {
// Modifier(element => maybeModifier.foreach(asModifier(_).apply(element)))
// }

/** Create a modifier that applies each of the modifiers in a seq */
implicit def seqToModifier[A, Collection[_], El <: ReactiveElement.Base](
Expand All @@ -101,7 +100,7 @@ trait Implicits extends Implicits.LowPriorityImplicits with CompositeValueMapper
implicit asModifier: A => Modifier[El],
renderableSeq: RenderableSeq[Collection]
): Modifier[El] = {
Modifier(element => renderableSeq.toSeq(modifiers).foreach(asModifier(_).apply(element)))
Modifier(element => renderableSeq.foreach(modifiers)(asModifier(_).apply(element)))
}

// The various collection-to-modifier conversions below are cheaper and better equivalents of
Expand All @@ -118,9 +117,9 @@ trait Implicits extends Implicits.LowPriorityImplicits with CompositeValueMapper

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

implicit def nodeOptionToModifier(nodes: Option[ChildNode.Base]): Modifier.Base = {
Modifier(element => nodes.foreach(_.apply(element)))
}
// implicit def nodeOptionToModifier(nodes: Option[ChildNode.Base]): Modifier.Base = {
// Modifier(element => nodes.foreach(_.apply(element)))
// }

// #Note: the case of Collection[Component] is covered by `seqToModifier` above
implicit def nodeSeqToModifier[Collection[_]](
Expand All @@ -129,8 +128,7 @@ trait Implicits extends Implicits.LowPriorityImplicits with CompositeValueMapper
implicit renderableSeq: RenderableSeq[Collection]
): Modifier.Base = {
Modifier { element =>
val nodesSeq = renderableSeq.toSeq(nodes)
nodesSeq.foreach(_.apply(element))
renderableSeq.foreach(nodes)(_.apply(element))
}
}
}
Expand Down Expand Up @@ -191,13 +189,13 @@ object Implicits {

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

implicit def componentOptionToInserter[Component](
maybeComponent: Option[Component]
)(
implicit renderableNode: RenderableNode[Component]
): StaticChildrenInserter = {
componentSeqToInserter(maybeComponent.toList)
}
// implicit def componentOptionToInserter[Component](
// maybeComponent: Option[Component]
// )(
// implicit renderableNode: RenderableNode[Component]
// ): StaticChildrenInserter = {
// componentSeqToInserter(maybeComponent.toList)
// }

implicit def componentSeqToInserter[Collection[_], Component](
components: Collection[Component]
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/raquo/laminar/keys/AriaAttr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.raquo.laminar.keys

import com.raquo.airstream.core.Source
import com.raquo.laminar.DomApi
import com.raquo.laminar.api.L.{Element, optionToSetter}
import com.raquo.laminar.api.L.{Element, seqToSetter}
import com.raquo.laminar.codecs.Codec
import com.raquo.laminar.modifiers.KeySetter.AriaAttrSetter
import com.raquo.laminar.modifiers.KeyUpdater.AriaAttrUpdater
Expand All @@ -29,7 +29,7 @@ class AriaAttr[V](
}

def maybe(value: Option[V]): Setter[Element] = {
optionToSetter(value.map(v => this := v))
seqToSetter[Option, Element](value.map(v => this := v))
}

def <--(values: Source[V]): AriaAttrUpdater[V] = {
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/raquo/laminar/keys/DerivedStyleProp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.raquo.laminar.keys

import com.raquo.airstream.core.Source
import com.raquo.laminar.DomApi
import com.raquo.laminar.api.L.{HtmlElement, optionToSetter}
import com.raquo.laminar.api.L.{HtmlElement, seqToSetter}
import com.raquo.laminar.modifiers.KeySetter.StyleSetter
import com.raquo.laminar.modifiers.KeyUpdater.DerivedStyleUpdater
import com.raquo.laminar.modifiers.{KeySetter, KeyUpdater, Setter}
Expand All @@ -27,7 +27,7 @@ class DerivedStyleProp[InputV](
}

def maybe(value: Option[InputV]): Setter[HtmlElement] = {
optionToSetter(value.map(v => this := v))
seqToSetter[Option, HtmlElement](value.map(v => this := v))
}

def <--(values: Source[InputV]): DerivedStyleUpdater[InputV] = {
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/raquo/laminar/keys/HtmlAttr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.raquo.laminar.keys

import com.raquo.airstream.core.Source
import com.raquo.laminar.DomApi
import com.raquo.laminar.api.L.{HtmlElement, optionToSetter}
import com.raquo.laminar.api.L.{HtmlElement, seqToSetter}
import com.raquo.laminar.codecs.Codec
import com.raquo.laminar.modifiers.KeySetter.HtmlAttrSetter
import com.raquo.laminar.modifiers.KeyUpdater.HtmlAttrUpdater
Expand All @@ -23,7 +23,7 @@ class HtmlAttr[V](
}

def maybe(value: Option[V]): Setter[HtmlElement] = {
optionToSetter(value.map(v => this := v))
seqToSetter[Option, HtmlElement](value.map(v => this := v))
}

def :=(value: V): HtmlAttrSetter[V] = {
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/raquo/laminar/keys/HtmlProp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.raquo.laminar.keys

import com.raquo.airstream.core.Source
import com.raquo.laminar.DomApi
import com.raquo.laminar.api.L.{HtmlElement, optionToSetter}
import com.raquo.laminar.api.L.{HtmlElement, seqToSetter}
import com.raquo.laminar.codecs.Codec
import com.raquo.laminar.modifiers.KeySetter.PropSetter
import com.raquo.laminar.modifiers.KeyUpdater.PropUpdater
Expand Down Expand Up @@ -31,7 +31,7 @@ class HtmlProp[V, DomV](
}

def maybe(value: Option[V]): Setter[HtmlElement] = {
optionToSetter(value.map(this := _))
seqToSetter[Option, HtmlElement](value.map(this := _))
}

def <--(values: Source[V]): PropUpdater[V, DomV] = {
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/raquo/laminar/keys/StyleProp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.raquo.laminar.keys

import com.raquo.airstream.core.Source
import com.raquo.laminar.DomApi
import com.raquo.laminar.api.L.{HtmlElement, optionToSetter}
import com.raquo.laminar.api.L.{HtmlElement, seqToSetter}
import com.raquo.laminar.defs.styles.traits.GlobalKeywords
import com.raquo.laminar.modifiers.KeySetter.StyleSetter
import com.raquo.laminar.modifiers.KeyUpdater.StyleUpdater
Expand All @@ -25,7 +25,7 @@ class StyleProp[V](
}

def maybe(value: Option[V | String]): Setter[HtmlElement] = {
optionToSetter(value.map(v => this := v))
seqToSetter[Option, HtmlElement](value.map(v => this := v))
}

/** Source[V] and Source[String] are of course also accepted. */
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/com/raquo/laminar/keys/SvgAttr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.raquo.laminar.keys

import com.raquo.airstream.core.Source
import com.raquo.laminar.DomApi
import com.raquo.laminar.api.L.{SvgElement, optionToSetter}
import com.raquo.laminar.api.L.{SvgElement, seqToSetter}
import com.raquo.laminar.codecs.Codec
import com.raquo.laminar.modifiers.KeySetter.SvgAttrSetter
import com.raquo.laminar.modifiers.KeyUpdater.SvgAttrUpdater
Expand Down Expand Up @@ -36,7 +36,7 @@ class SvgAttr[V](
}

def maybe(value: Option[V]): Setter[SvgElement] = {
optionToSetter(value.map(v => this := v))
seqToSetter[Option, SvgElement](value.map(v => this := v))
}

def <--(values: Source[V]): SvgAttrUpdater[V] = {
Expand Down
56 changes: 45 additions & 11 deletions src/main/scala/com/raquo/laminar/modifiers/RenderableSeq.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import scala.scalajs.js
trait RenderableSeq[-Collection[_]] {

def toSeq[A](values: Collection[A]): laminar.Seq[A]

def foreach[A](values: Collection[A])(f: A => Unit): Unit
}

object RenderableSeq {
Expand All @@ -17,48 +19,80 @@ object RenderableSeq {
override def toSeq[A](values: collection.Seq[A]): laminar.Seq[A] = {
laminar.Seq.from(values)
}

override def foreach[A](values: collection.Seq[A])(f: A => Unit): Unit = {
values.foreach(f)
}
}

implicit object scalaArrayRenderable extends RenderableSeq[scala.Array] {
override def toSeq[A](values: scala.Array[A]): laminar.Seq[A] = {
laminar.Seq.from(values)
}

override def foreach[A](values: Array[A])(f: A => Unit): Unit = {
values.foreach(f)
}
}

implicit object jsArrayRenderable extends RenderableSeq[JsArray] {
override def toSeq[A](values: JsArray[A]): laminar.Seq[A] = {
laminar.Seq.from(values)
}

override def foreach[A](values: JsArray[A])(f: A => Unit): Unit = {
values.forEach(f)
}
}

implicit object sjsArrayRenderable extends RenderableSeq[js.Array] {
override def toSeq[A](values: js.Array[A]): laminar.Seq[A] = {
laminar.Seq.from(values)
}

override def foreach[A](values: js.Array[A])(f: A => Unit): Unit = {
values.foreach(f)
}
}

implicit object jsVectorRenderable extends RenderableSeq[JsVector] {
override def toSeq[A](values: JsVector[A]): laminar.Seq[A] = {
laminar.Seq.from(values)
}

override def foreach[A](values: JsVector[A])(f: A => Unit): Unit = {
values.forEach(f)
}
}

implicit object laminarSeqRenderable extends RenderableSeq[laminar.Seq] {
override def toSeq[A](values: laminar.Seq[A]): laminar.Seq[A] = {
values
}

override def foreach[A](values: laminar.Seq[A])(f: A => Unit): Unit = {
values.foreach(f)
}
}

// object optionRenderable extends RenderableSeq[Option] {
// override def toSeq[A](maybeValue: Option[A]): laminar.Seq[A] = {
// laminar.Seq.from(maybeValue.toList)
// }
// }
//
// object jsUndefOrRenderable extends RenderableSeq[js.UndefOr] {
// override def toSeq[A](maybeValue: js.UndefOr[A]): laminar.Seq[A] = {
// laminar.Seq.from(JsArray.from(maybeValue))
// }
// }
implicit object optionRenderable extends RenderableSeq[Option] {
override def toSeq[A](maybeValue: Option[A]): laminar.Seq[A] = {
laminar.Seq.from(maybeValue.toList)
}

override def foreach[A](values: Option[A])(f: A => Unit): Unit = {
values.foreach(f)
}
}

implicit object jsUndefOrRenderable extends RenderableSeq[js.UndefOr] {
override def toSeq[A](maybeValue: js.UndefOr[A]): laminar.Seq[A] = {
laminar.Seq.from(JsArray.from(maybeValue))
}

override def foreach[A](values: js.UndefOr[A])(f: A => Unit): Unit = {
values.foreach(f)
}
}

}
20 changes: 14 additions & 6 deletions src/main/scala/com/raquo/laminar/receivers/ChildrenReceiver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,20 @@ object ChildrenReceiver {
implicit renderableNode: RenderableNode[Component],
renderableSeq: RenderableSeq[Collection]
): DynamicInserter = {
ChildrenInserter(
childrenSource.toObservable,
renderableSeq,
renderableNode,
initialHooks = js.undefined
)
// Route the Option case to simpler more efficient child.maybe receiver.
if (renderableSeq == RenderableSeq.optionRenderable) {
ChildOptionReceiver <-- childrenSource.asInstanceOf[Source[Option[Component]]]
// #TODO child.maybe can't handle js.UndefOr yet
// } else if (renderableSeq == RenderableSeq.jsUndefOrRenderable) {
// ChildOptionReceiver <-- childrenSource.asInstanceOf[Source[js.UndefOr[Component]]]
} else {
ChildrenInserter(
childrenSource.toObservable,
renderableSeq,
renderableNode,
initialHooks = js.undefined
)
}
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/test/scala/com/raquo/laminar/tests/ChildrenReceiverSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.raquo.laminar.fixtures.AirstreamFixtures.Effect
import com.raquo.laminar.modifiers.RenderableNode
import com.raquo.laminar.nodes.ChildNode
import com.raquo.laminar.utils.UnitSpec
import org.scalajs.dom
import org.scalatest.BeforeAndAfter

import scala.collection.{immutable, mutable}
Expand Down Expand Up @@ -1205,4 +1206,22 @@ class ChildrenReceiverSpec extends UnitSpec with BeforeAndAfter {
)
}

it("Supports Option-s as Seq-s") {
val childBus = new EventBus[Option[ChildNode.Base]]
val childSource = childBus.events

mount(div("Hello, ", children <-- childSource))
expectNode(div.of("Hello, ", sentinel))

withClue("First event:") {
childBus.writer.onNext(Some(span(text1)))
expectNode(div.of("Hello, ", sentinel, span of text1))
}

withClue("Second event, changing node type (span->div):") {
childBus.writer.onNext(Some(div(text2)))
expectNode(div.of("Hello, ", sentinel, div of text2))
}
}

}

0 comments on commit 90d238c

Please sign in to comment.