Skip to content

Commit

Permalink
New: conditional syntax for child, children, and text receivers
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Nov 27, 2023
1 parent c68ad25 commit 3163c6a
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.raquo.laminar.modifiers

import com.raquo.laminar.nodes.ChildNode
import com.raquo.laminar.nodes.{ChildNode, CommentNode}

import scala.annotation.implicitNotFound
import scala.collection.immutable
Expand All @@ -18,7 +18,7 @@ import scala.collection.immutable
*
* See also – [[RenderableText]]
*/
@implicitNotFound("Implicit instance of RenderableNode[${Component}] not found. If `${Component}` is a custom component that you want to render as a node / element, define an implicit RenderableNode[${Component}] instance for it. For rendering as a string or primitive value, define RenderableText[${Component}] instead, and perhaps use `child.text <--` instead of `child <-- ...`.")
@implicitNotFound("Implicit instance of RenderableNode[${Component}] not found. If `${Component}` is a custom component that you want to render as a node / element, define an implicit RenderableNode[${Component}] instance for it. For rendering as a string or primitive value, define RenderableText[${Component}] instead, and use `text <--` instead of `child <-- ...`.")
trait RenderableNode[-Component] {

/** For every component, this MUST ALWAYS return the exact same node reference. */
Expand Down
20 changes: 19 additions & 1 deletion src/main/scala/com/raquo/laminar/receivers/ChildReceiver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,31 @@ object ChildReceiver {

val text: ChildTextReceiver.type = ChildTextReceiver

/** Usage example: child(element) <-- signalOfBoolean */
def apply(node: ChildNode.Base): LockedChildReceiver = {
new LockedChildReceiver(node)
}

def <--(childSource: Source[ChildNode.Base]): Inserter.Base = {
ChildInserter(childSource.toObservable, RenderableNode.nodeRenderable)
}

implicit class RichChildReceiver(val self: ChildReceiver.type) extends AnyVal {

def <--[Component](childSource: Source[Component])(implicit renderable: RenderableNode[Component]): Inserter.Base = {
/** Usage example: child(component) <-- signalOfBoolean */
def apply[Component](
component: Component
)(
implicit renderable: RenderableNode[Component]
): LockedChildReceiver = {
new LockedChildReceiver(renderable.asNode(component))
}

def <--[Component](
childSource: Source[Component]
)(
implicit renderable: RenderableNode[Component]
): Inserter.Base = {
ChildInserter(childSource.toObservable, renderable)
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/main/scala/com/raquo/laminar/receivers/ChildTextReceiver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import com.raquo.laminar.nodes.TextNode

object ChildTextReceiver {

/** Usage example: text("hello") <-- signalOfBoolean */
def apply(text: String): LockedChildTextReceiver = {
new LockedChildTextReceiver(text)
}

/** Usage example: text(textLikeThing) <-- signalOfBoolean */
def apply[TextLike](text: TextLike)(implicit renderable: RenderableText[TextLike]): LockedChildTextReceiver = {
// #TODO[Perf] is there a better way to handle RenderableText.textNodeRenderable?
// (see comment about it in the `<--` method)
new LockedChildTextReceiver(renderable.asString(text))
}

def <--(textSource: Source[String]): Inserter.Base = {
ChildTextInserter(textSource.toObservable, RenderableText.stringRenderable)
}
Expand Down
33 changes: 33 additions & 0 deletions src/main/scala/com/raquo/laminar/receivers/ChildrenReceiver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ object ChildrenReceiver {

val command: ChildrenCommandReceiver.type = ChildrenCommandReceiver

/** Example usage: children(listOfNodes) <-- signalOfBoolean */
def apply(nodes: immutable.Seq[ChildNode.Base]): LockedChildrenReceiver = {
new LockedChildrenReceiver(nodes)
}

/** Example usage: children(listOfComponents) <-- signalOfBoolean */
def apply[Component](
components: immutable.Seq[Component]
)(
implicit renderable: RenderableNode[Component]
): LockedChildrenReceiver = {
new LockedChildrenReceiver(renderable.asNodeSeq(components))
}

// Note: currently this <-- method requires an observable of an
// **immutable** Seq, but if needed, I might be able to implement
// a version that works with arrays and mutable Seq-s too.
Expand All @@ -26,4 +40,23 @@ object ChildrenReceiver {
): Inserter.Base = {
ChildrenInserter(childrenSource.toObservable, renderableNode)
}

implicit class RichChildrenReceiver(val self: ChildrenReceiver.type) extends AnyVal {

/** Example usage: children(node1, node2) <-- signalOfBoolean */
def apply(nodes: ChildNode.Base*): LockedChildrenReceiver = {
// #TODO[Scala 2.12] - toList is only needed because in Scala 2.12 varargs are (non-immutable) Seq
new LockedChildrenReceiver(nodes.toList)
}

/** Example usage: children(component1, component2) <-- signalOfBoolean */
def apply[Component](
components: Component*
)(
implicit renderable: RenderableNode[Component]
): LockedChildrenReceiver = {
// #TODO[Scala 2.12] - toList is only needed because in Scala 2.12 varargs are (non-immutable) Seq
new LockedChildrenReceiver(renderable.asNodeSeq(components.toList))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.raquo.laminar.receivers

import com.raquo.airstream.core.Source
import com.raquo.laminar.api.L.child
import com.raquo.laminar.modifiers.Inserter
import com.raquo.laminar.nodes.{ChildNode, CommentNode}

class LockedChildReceiver(
node: ChildNode.Base
) {

/** If `include` is true, the node will be added. Otherwise, an empty node will be added. */
@inline def apply(include: Boolean): ChildNode.Base = {
this := include
}

/** If `include` is true, the node will be added. Otherwise, an empty node will be added. */
def :=(include: Boolean): ChildNode.Base = {
if (include) node else new CommentNode("")
}

/** If `includeSource` emits true, node will be added. Otherwise, it will be removed. */
def <--(includeSource: Source[Boolean]): Inserter.Base = {
val emptyNode = new CommentNode("")
child <-- includeSource.toObservable.map(if (_) node else emptyNode)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.raquo.laminar.receivers

import com.raquo.airstream.core.Source
import com.raquo.laminar.api.L.child
import com.raquo.laminar.modifiers.Inserter
import com.raquo.laminar.nodes.{ChildNode, CommentNode, TextNode}

class LockedChildTextReceiver(
val text: String
) {

/** If `include` is true, the text will be added. Otherwise, an empty node will be added. */
@inline def apply(include: Boolean): ChildNode.Base = {
this := include
}

/** If `include` is true, the text will be added. Otherwise, an empty node will be added. */
def :=(include: Boolean): ChildNode.Base = {
if (include) new TextNode(text) else new CommentNode("")
}

/** If `includeSource` emits true, text will be added. Otherwise, it will be removed. */
def <--(includeSource: Source[Boolean]): Inserter.Base = {
child.text <-- includeSource.toObservable.map(if (_) text else "")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.raquo.laminar.receivers

import com.raquo.airstream.core.Source
import com.raquo.laminar.api.L.children
import com.raquo.laminar.modifiers.Inserter
import com.raquo.laminar.nodes.ChildNode

import scala.collection.immutable

class LockedChildrenReceiver(
val nodes: immutable.Seq[ChildNode.Base]
) {

/** If `include` is true, the nodes will be added. */
@inline def apply(include: Boolean): immutable.Seq[ChildNode.Base] = {
this := include
}

/** If `include` is true, the nodes will be added. */
def :=(include: Boolean): immutable.Seq[ChildNode.Base] = {
if (include) nodes else Nil
}

/** If `includeSource` emits true, node will be added. Otherwise, it will be removed. */
def <--(includeSource: Source[Boolean]): Inserter.Base = {
children <-- includeSource.toObservable.map(if (_) nodes else Nil)
}

}
79 changes: 79 additions & 0 deletions src/test/scala/com/raquo/laminar/ChildReceiverSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.raquo.laminar
import com.raquo.domtestutils.matching.ExpectedNode
import com.raquo.laminar.api.L
import com.raquo.laminar.api.L._
import com.raquo.laminar.modifiers.RenderableNode
import com.raquo.laminar.nodes.ChildNode
import com.raquo.laminar.utils.UnitSpec
import org.scalajs.dom
Expand Down Expand Up @@ -433,4 +434,82 @@ class ChildReceiverSpec extends UnitSpec {
)
}

it("locked to one child") {

val v = Var(true)

val el = div(
child(span("nope")) := false,
child(span("yep")) := true,
child(span("hello")) <-- v
)

// --

mount(el)

expectNode(
div.of(emptyCommentNode, span.of("yep"), sentinel, span.of("hello"))
)

// --

v.set(false)

expectNode(
div.of(emptyCommentNode, span.of("yep"), sentinel, sentinel)
)

// --

v.set(true)

expectNode(
div.of(emptyCommentNode, span.of("yep"), sentinel, span.of("hello"))
)
}

it("locked to one component") {

class Component(text: String) {
val node: Span = span(text)
}

implicit val componentRenderable: RenderableNode[Component] =
RenderableNode(_.node, _.map(_.node), _.map(_.node), _.map(_.node))


val v = Var(true)

val el = div(
child(new Component("nope")) := false,
child(new Component("yep")) := true,
child(new Component("hello")) <-- v
)

// --

mount(el)

expectNode(
div.of(emptyCommentNode, span.of("yep"), sentinel, span.of("hello"))
)

// --

v.set(false)

expectNode(
div.of(emptyCommentNode, span.of("yep"), sentinel, sentinel)
)

// --

v.set(true)

expectNode(
div.of(emptyCommentNode, span.of("yep"), sentinel, span.of("hello"))
)
}

}
75 changes: 75 additions & 0 deletions src/test/scala/com/raquo/laminar/ChildTextReceiverSpec.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.raquo.laminar

import com.raquo.laminar.api.L._
import com.raquo.laminar.modifiers.RenderableText
import com.raquo.laminar.utils.UnitSpec

class ChildTextReceiverSpec extends UnitSpec {
Expand Down Expand Up @@ -130,4 +131,78 @@ class ChildTextReceiverSpec extends UnitSpec {
)
)
}

it("locked to one string") {

val v = Var(true)

val el = div(
text("nope") := false,
text("yep") := true,
text("hello") <-- v
)

// --

mount(el)

expectNode(
div.of(emptyCommentNode, "yep", "hello")
)

// --

v.set(false)

expectNode(
div.of(emptyCommentNode, "yep", "")
)

// --

v.set(true)

expectNode(
div.of(emptyCommentNode, "yep", "hello")
)
}

it("locked to one textLike") {

val v = Var(true)

class TextLike(val str: String)

implicit val renderable: RenderableText[TextLike] = RenderableText(_.str)

val el = div(
text(new TextLike("nope")) := false,
text(new TextLike("yep")) := true,
text(new TextLike("hello")) <-- v
)

// --

mount(el)

expectNode(
div.of(emptyCommentNode, "yep", "hello")
)

// --

v.set(false)

expectNode(
div.of(emptyCommentNode, "yep", "")
)

// --

v.set(true)

expectNode(
div.of(emptyCommentNode, "yep", "hello")
)
}
}
Loading

0 comments on commit 3163c6a

Please sign in to comment.