From 6f012b67960eae9b5220122f4da8898f9af9b9a1 Mon Sep 17 00:00:00 2001 From: Paul-Henri Froidmont Date: Tue, 16 Sep 2025 03:16:50 +0200 Subject: [PATCH] Initial support for JS commands --- core/src/scalive/HtmlElement.scala | 4 +-- core/src/scalive/JS.scala | 53 ++++++++++++++++++++++++++++++ example/src/HomeLiveView.scala | 4 +-- example/src/ListLiveView.scala | 6 +++- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 core/src/scalive/JS.scala diff --git a/core/src/scalive/HtmlElement.scala b/core/src/scalive/HtmlElement.scala index 9ccb1ba..ea0d485 100644 --- a/core/src/scalive/HtmlElement.scala +++ b/core/src/scalive/HtmlElement.scala @@ -64,10 +64,10 @@ class HtmlAttr[V](val name: String, val codec: Codec[V, String]): class HtmlAttrJsonValue(val name: String): - def :=[V: JsonCodec](value: V): Mod.Attr = + def :=[V: JsonEncoder](value: V): Mod.Attr = Mod.Attr.Static(name, value.toJson, isJson = true) - def :=[V: JsonCodec](value: Dyn[V]): Mod.Attr = + def :=[V: JsonEncoder](value: Dyn[V]): Mod.Attr = Mod.Attr.Dyn(name, value(_.toJson), isJson = true) sealed trait Mod diff --git a/core/src/scalive/JS.scala b/core/src/scalive/JS.scala new file mode 100644 index 0000000..fed1f4b --- /dev/null +++ b/core/src/scalive/JS.scala @@ -0,0 +1,53 @@ +package scalive + +import zio.json.* +import zio.json.ast.Json + +val JS: JSCommands.JSCommand = JSCommands.empty + +object JSCommands: + opaque type JSCommand = List[Json] + + def empty: JSCommand = List.empty + + object JSCommand: + given JsonEncoder[JSCommand] = + JsonEncoder[Json].contramap(ops => Json.Arr(ops.reverse*)) + + private def classNames(names: String) = names.split("\\s+") + + extension (ops: JSCommand) + private def addOp[A: JsonEncoder](kind: String, args: A) = + (kind, args).toJsonAST.getOrElse(throw new IllegalArgumentException()) :: ops + + def toggleClass( + names: String, + to: String = "", + transition: String | (String, String, String) = "", + time: Int = 200, + blocking: Boolean = true + ) = + ops.addOp( + "toggle_class", + Args.ToggleClass( + classNames(names), + Some(to).filterNot(_.isBlank), + transition match + case "" => None + case names: String => Some(classNames(names)) + case t: (String, String, String) => Some(t.toList), + Some(time).filterNot(_ == 200), + Some(blocking).filterNot(_ == true) + ) + ) + + private object Args: + final case class ToggleClass( + names: Seq[String], + to: Option[String], + transition: Option[Seq[String]], + time: Option[Int], + blocking: Option[Boolean]) + derives JsonEncoder + +end JSCommands diff --git a/example/src/HomeLiveView.scala b/example/src/HomeLiveView.scala index 7b5f984..241f02a 100644 --- a/example/src/HomeLiveView.scala +++ b/example/src/HomeLiveView.scala @@ -4,8 +4,8 @@ import zio.stream.ZStream class HomeLiveView() extends LiveView[String, Unit]: val links = List( - "/counter" -> "Counter", - "/list" -> "List" + "/counter" -> "Counter", + "/list?q=test" -> "List" ) def init = ZIO.succeed(()) diff --git a/example/src/ListLiveView.scala b/example/src/ListLiveView.scala index 9beb8d8..6d92063 100644 --- a/example/src/ListLiveView.scala +++ b/example/src/ListLiveView.scala @@ -54,7 +54,11 @@ class ListLiveView(someParam: String) extends LiveView[Msg, Model]: phx.click := Msg.IncAge(1), "Inc age" ), - span(cls := "grow") + span(cls := "grow"), + button( + phx.click := JS.toggleClass("bg-red-500 border-5"), + "Toggle color" + ) ) )