armanbilge / calico

Pure, reactive UI library for Scala.js
https://armanbilge.github.io/calico
Apache License 2.0
109 stars 8 forks source link

Feature idea: modifier/node interpolator #401

Open kubukoz opened 6 hours ago

kubukoz commented 6 hours ago

Sometimes you just want to do:

div(
  s"today is $myDateSignal and I have $appleCountSignal apples"
)

but what you actually have to do is:

div(
  "today is ", myDateSignal, " and I have ", appleCountSignal, " apples"
)

Alternatively, I suppose you can combine signals:

div(
  (myDateSignal, appleCountSignal).mapN { (date, apples) => 
    s"today is $date and I have $apples apples"
  }
)

but it's still a bit cumbersome, works only with text nodes and will result in the entire text updating, rather than just the small pieces that change.

Perhaps it would make it worth adding an interpolator like nodes"..." which will serve as a Modifier-based Show?

kubukoz commented 6 hours ago

This seems to be doable in userland so maybe I'll try making it first to see if it's even possible with the current API...

kubukoz commented 6 hours ago

ok, hear me out:

type IntersperseStrings[T <: Tuple] <: Tuple =
  T match {
    case EmptyTuple => String *: EmptyTuple
    case (t *: ts)  => String *: t *: IntersperseStrings[ts]
  }

inline def intersperseStrings[T <: Tuple](t: T, strings: Seq[String]): IntersperseStrings[T] =
  inline scala.compiletime.erasedValue[T] match {
    case _: EmptyTuple => strings.head *: EmptyTuple
    case _: (head *: tail) =>
      inline t match {
        case v: (`head` *: `tail`) =>
          val (h *: t) = v
          strings.head *: h *: intersperseStrings(t, strings.tail)
      }
  }

extension (sc: StringContext) {

  inline def nodes[M <: Tuple, E <: HtmlElement[IO]](
    arg: M
  )(
    using Modifier[IO, E, M]
  ): IntersperseStrings[M] = {
    StringContext.checkLengths(arg.toList, sc.parts)
    intersperseStrings(arg, sc.parts.map(StringContext.processEscapes))
  }

}

  def go(myDateSignal: Signal[IO, String], appleCountSignal: Signal[IO, String]) = div(
    nodes"today is $myDateSignal and I have $appleCountSignal apples, let me say that again $appleCountSignal apples"
  )

  def go2(myDateSignal: Signal[IO, String], appleCountSignal: Signal[IO, String]) = div(
    nodes"today is $myDateSignal and I have $appleCountSignal apples"
  )

Probably missing some edge cases.

kubukoz commented 5 hours ago

Need a special case for a no-tuple scenario. Also, it doesn't work if you actually try to interpolate a tuple :/

nodes"today is ${(myDateSignal, "foo")}"

The interpolator will see this as one tuple of length 2, and there are 2 parts in the StringContext - the length check will fail.