From aaa1a957285b3f5cfdaec1ba29dc1e7492cb2448 Mon Sep 17 00:00:00 2001 From: Nikita Gazarov Date: Thu, 16 Jan 2025 01:18:17 -0800 Subject: [PATCH] New: modSeqWith and seqWith. Fixes #146 --- .../scala/com/raquo/laminar/api/Laminar.scala | 46 ++++++++++++++++++- .../raquo/laminar/tests/basic/ModSpec.scala | 36 +++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/main/scala/com/raquo/laminar/api/Laminar.scala b/src/main/scala/com/raquo/laminar/api/Laminar.scala index b8b5770..0220556 100644 --- a/src/main/scala/com/raquo/laminar/api/Laminar.scala +++ b/src/main/scala/com/raquo/laminar/api/Laminar.scala @@ -1,7 +1,7 @@ package com.raquo.laminar.api import com.raquo.airstream.web.DomEventStream -import com.raquo.laminar.{nodes, DomApi} +import com.raquo.laminar.{DomApi, nodes} import com.raquo.laminar.defs.attrs.{AriaAttrs, HtmlAttrs, SvgAttrs} import com.raquo.laminar.defs.complex.{ComplexHtmlKeys, ComplexSvgKeys} import com.raquo.laminar.defs.eventProps.{DocumentEventProps, GlobalEventProps, WindowEventProps} @@ -132,6 +132,49 @@ with Implicits { new DetachedRoot(rootNode, activateNow) } + /** + * This method returns a Seq of modifiers, each of which is created from the same `input`. + * + * This is convenient when you e.g. want to bind multiple listeners to + * the same observable, keeping related together, without repeating yourself: + * + * {{{ + * div( + * modSeqWith(coordinatesSignal.map(process).distinct)( + * left.px <-- _.map(_.x), + * top.px <-- _.map(_.y), + * _.map(_.z) --> zObserver + * ) + * ) + * }}} + * + * You can use the output of such a modSeqWith call directly in Laminar because + * Laminar implicitly converts `Seq[Modifier]` to `Modifier`. + * + * The above snippet is equivalent to: + * + * {{{ + * lazy val coords = coordinatesSignal.map(process).distinct + * div( + * left.px <-- coords.map(_.x), + * top.px <-- coords.map(_.y), + * coords.map(_.z) --> zObserver + * ) + * }}} + * + * See also [[seqWith]] for a more generalized version. + */ + def modSeqWith[El <: Element, In](input: In)(outputs: (In => Modifier[El])*): Seq[Modifier[El]] = { + outputs.map(_(input)) + } + + /** Like [[modSeqWith]], but works for arbitrary output types. + * Downside is that type inference that requires Modifier implicit conversions may not work. + */ + def seqWith[In, Out](input: In)(outputs: (In => Out)*): Seq[Out] = { + outputs.map(_(input)) + } + /** * Get a Seq of modifiers. You could just use Seq(...), but this helper * has better type inference in some cases. @@ -241,4 +284,5 @@ with Implicits { ): Binder[ReactiveHtmlElement[Ref]] = { InputController.controlled(listener, updater) } + } diff --git a/src/test/scala/com/raquo/laminar/tests/basic/ModSpec.scala b/src/test/scala/com/raquo/laminar/tests/basic/ModSpec.scala index 640867a..0fe95b0 100644 --- a/src/test/scala/com/raquo/laminar/tests/basic/ModSpec.scala +++ b/src/test/scala/com/raquo/laminar/tests/basic/ModSpec.scala @@ -3,6 +3,8 @@ package com.raquo.laminar.tests.basic import com.raquo.laminar.api.L._ import com.raquo.laminar.utils.UnitSpec +import scala.scalajs.js + class ModSpec extends UnitSpec { it("when keyword") { @@ -77,4 +79,38 @@ class ModSpec extends UnitSpec { ) } + case class Coordinates(x: Int, y: Int, z: Int) + + it("modSeqWith") { + var lastZ: js.UndefOr[Int] = js.undefined + val zObserver = Observer[Int](lastZ = _) + + val el = div( + modSeqWith(Val(Coordinates(1, 2, 3)))( + _ => "hello", // testing type inference that depends on implicits + left.px <-- _.map(_.x), + top.px <-- _.map(_.y), + _.map(_.z) --> zObserver, + child.text <-- _.map(_.toString) + ) + ) + + assertEquals(lastZ, js.undefined) + + // -- + + mount(el) + + expectNode( + div.of( + "hello", + left is "1px", + top is "2px", + "Coordinates(1,2,3)" + ) + ) + + assertEquals(lastZ, 3) + } + }