mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Support message parameters
This commit is contained in:
parent
763788fb89
commit
681feced9f
16 changed files with 277 additions and 106 deletions
|
|
@ -27,6 +27,7 @@ class DomDefsGenerator(baseOutputDirectoryPath: String):
|
||||||
override def scalaJsDomImport: String = ""
|
override def scalaJsDomImport: String = ""
|
||||||
override def tagKeysPackagePath: String = "scalive"
|
override def tagKeysPackagePath: String = "scalive"
|
||||||
override def keysPackagePath: String = "scalive"
|
override def keysPackagePath: String = "scalive"
|
||||||
|
|
||||||
override def generateTagsTrait(
|
override def generateTagsTrait(
|
||||||
tagType: TagType,
|
tagType: TagType,
|
||||||
defGroups: List[(String, List[TagDef])],
|
defGroups: List[(String, List[TagDef])],
|
||||||
|
|
@ -186,7 +187,14 @@ class DomDefsGenerator(baseOutputDirectoryPath: String):
|
||||||
|
|
||||||
val fileContent = generator.generateAttrsTrait(
|
val fileContent = generator.generateAttrsTrait(
|
||||||
defGroups = defGroups.htmlAttrDefGroups.appended(
|
defGroups = defGroups.htmlAttrDefGroups.appended(
|
||||||
"Reflected Attributes" -> ReflectedHtmlAttrDefs.defs.map(_.toAttrDef)
|
"Reflected Attributes" -> ReflectedHtmlAttrDefs.defs
|
||||||
|
.map(d =>
|
||||||
|
d.scalaName match
|
||||||
|
case "defaultChecked" => d.copy(scalaName = "checked")
|
||||||
|
case "defaultSelected" => d.copy(scalaName = "selected")
|
||||||
|
case "defaultValue" => d.copy(scalaName = "value")
|
||||||
|
case _ => d
|
||||||
|
).map(_.toAttrDef)
|
||||||
),
|
),
|
||||||
printDefGroupComments = false,
|
printDefGroupComments = false,
|
||||||
traitCommentLines = Nil,
|
traitCommentLines = Nil,
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,11 @@ object DiffBuilder:
|
||||||
case Some((entries, keysCount)) =>
|
case Some((entries, keysCount)) =>
|
||||||
val static =
|
val static =
|
||||||
entries.collectFirst { case (_, Some(el)) => el.static }.getOrElse(List.empty)
|
entries.collectFirst { case (_, Some(el)) => el.static }.getOrElse(List.empty)
|
||||||
|
val includeStatic = !trackUpdates || keysCount == entries.count(_._2.isDefined)
|
||||||
List(
|
List(
|
||||||
Some(
|
Some(
|
||||||
Diff.Comprehension(
|
Diff.Comprehension(
|
||||||
static = if trackUpdates then Seq.empty else static,
|
static = if includeStatic then static else Seq.empty,
|
||||||
entries = entries.map {
|
entries = entries.map {
|
||||||
case (key, Some(el)) =>
|
case (key, Some(el)) =>
|
||||||
Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates))
|
Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates))
|
||||||
|
|
@ -58,7 +59,6 @@ object DiffBuilder:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
case None => List(None)
|
case None => List(None)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
end DiffBuilder
|
end DiffBuilder
|
||||||
|
|
|
||||||
|
|
@ -37,14 +37,17 @@ sealed trait Dyn[T]:
|
||||||
private[scalive] def callOnEveryChild(f: T => Unit): Unit
|
private[scalive] def callOnEveryChild(f: T => Unit): Unit
|
||||||
|
|
||||||
extension [T](parent: Dyn[List[T]])
|
extension [T](parent: Dyn[List[T]])
|
||||||
def splitByIndex(project: (Int, Dyn[T]) => HtmlElement): Mod =
|
// TODO fix
|
||||||
|
def splitBy[Key](key: T => Key)(project: (Key, Dyn[T]) => HtmlElement): Mod =
|
||||||
Mod.Content.DynSplit(
|
Mod.Content.DynSplit(
|
||||||
new SplitVar(
|
new SplitVar(
|
||||||
parent.apply(_.zipWithIndex),
|
parent,
|
||||||
key = _._2,
|
key = key,
|
||||||
project = (index, v) => project(index, v(_._1))
|
project = (k, v) => project(k, v)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
def splitByIndex(project: (Int, Dyn[T]) => HtmlElement): Mod =
|
||||||
|
parent(_.zipWithIndex).splitBy(_._2)((index, v) => project(index, v(_._1)))
|
||||||
|
|
||||||
private class Var[T] private (initial: T) extends Dyn[T]:
|
private class Var[T] private (initial: T) extends Dyn[T]:
|
||||||
private[scalive] var currentValue: T = initial
|
private[scalive] var currentValue: T = initial
|
||||||
|
|
@ -93,7 +96,6 @@ private class SplitVar[I, O, Key](
|
||||||
key: I => Key,
|
key: I => Key,
|
||||||
project: (Key, Dyn[I]) => O):
|
project: (Key, Dyn[I]) => O):
|
||||||
|
|
||||||
// Deleted elements have value none
|
|
||||||
private val memoized: mutable.Map[Key, (Var[I], O)] =
|
private val memoized: mutable.Map[Key, (Var[I], O)] =
|
||||||
mutable.Map.empty
|
mutable.Map.empty
|
||||||
|
|
||||||
|
|
@ -141,4 +143,6 @@ private class SplitVar[I, O, Key](
|
||||||
private[scalive] def callOnEveryChild(f: O => Unit): Unit =
|
private[scalive] def callOnEveryChild(f: O => Unit): Unit =
|
||||||
memoized.values.foreach((_, output) => f(output))
|
memoized.values.foreach((_, output) => f(output))
|
||||||
|
|
||||||
|
private[scalive] def currentValues: Iterable[O] = memoized.values.map(_._2)
|
||||||
|
|
||||||
end SplitVar
|
end SplitVar
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
|
import scalive.JSCommands.JSCommand
|
||||||
import scalive.Mod.Attr
|
import scalive.Mod.Attr
|
||||||
import scalive.Mod.Content
|
import scalive.Mod.Content
|
||||||
import scalive.codecs.BooleanAsAttrPresenceEncoder
|
import scalive.codecs.BooleanAsAttrPresenceEncoder
|
||||||
import scalive.codecs.Encoder
|
import scalive.codecs.Encoder
|
||||||
import zio.json.*
|
import zio.json.*
|
||||||
|
|
||||||
|
import java.util.Base64
|
||||||
|
import scala.util.Random
|
||||||
|
|
||||||
class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
|
class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
|
||||||
def static: Seq[String] = StaticBuilder.build(this)
|
def static: Seq[String] = StaticBuilder.build(this)
|
||||||
def attrMods: Seq[Mod.Attr] =
|
def attrMods: Seq[Mod.Attr] =
|
||||||
|
|
@ -25,6 +29,8 @@ class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
|
||||||
|
|
||||||
def prepended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.prependedAll(mod))
|
def prepended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.prependedAll(mod))
|
||||||
def apended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.appendedAll(mod))
|
def apended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.appendedAll(mod))
|
||||||
|
def findBinding[Msg](id: String): Option[Map[String, String] => Msg] =
|
||||||
|
mods.iterator.iterator.map(_.findBinding(id)).collectFirst { case Some(f) => f }
|
||||||
|
|
||||||
private[scalive] def syncAll(): Unit = mods.foreach(_.syncAll())
|
private[scalive] def syncAll(): Unit = mods.foreach(_.syncAll())
|
||||||
private[scalive] def setAllUnchanged(): Unit = dynamicMods.foreach(_.setAllUnchanged())
|
private[scalive] def setAllUnchanged(): Unit = dynamicMods.foreach(_.setAllUnchanged())
|
||||||
|
|
@ -62,13 +68,30 @@ class HtmlAttr[V](val name: String, val codec: Encoder[V, String]):
|
||||||
)
|
)
|
||||||
else Mod.Attr.Dyn(name, value(codec.encode))
|
else Mod.Attr.Dyn(name, value(codec.encode))
|
||||||
|
|
||||||
class HtmlAttrJsonValue(val name: String):
|
class HtmlAttrBinding(val name: String):
|
||||||
|
def apply(cmd: JSCommand): Mod.Attr =
|
||||||
|
Mod.Attr.JsBinding(name, cmd.toJson, cmd.bindings)
|
||||||
|
|
||||||
def :=[V: JsonEncoder](value: V): Mod.Attr =
|
def apply[Msg](msg: Msg): Mod.Attr =
|
||||||
Mod.Attr.Static(name, value.toJson, isJson = true)
|
apply(_ => msg)
|
||||||
|
|
||||||
def :=[V: JsonEncoder](value: Dyn[V]): Mod.Attr =
|
def apply[Msg](f: Map[String, String] => Msg): Mod.Attr =
|
||||||
Mod.Attr.Dyn(name, value(_.toJson), isJson = true)
|
Mod.Attr.Binding(
|
||||||
|
name,
|
||||||
|
Base64.getUrlEncoder().withoutPadding().encodeToString(Random().nextBytes(12)),
|
||||||
|
f
|
||||||
|
)
|
||||||
|
def withValue[Msg](f: String => Msg): Mod.Attr =
|
||||||
|
apply(m => f(m("value")))
|
||||||
|
|
||||||
|
trait BindingAdapter[F, Msg]:
|
||||||
|
def createMessage(f: F): Map[String, String] => Msg
|
||||||
|
object BindingAdapter:
|
||||||
|
given fromString[Msg]: BindingAdapter[String => Msg, Msg] = f => m => f(m("value"))
|
||||||
|
given fromMap[Msg]: BindingAdapter[Map[String, String] => Msg, Msg] = f => f
|
||||||
|
|
||||||
|
final case class BindingParams(params: Map[String, String]):
|
||||||
|
def apply(key: String) = params.apply(key)
|
||||||
|
|
||||||
sealed trait Mod
|
sealed trait Mod
|
||||||
sealed trait StaticMod extends Mod
|
sealed trait StaticMod extends Mod
|
||||||
|
|
@ -76,8 +99,12 @@ sealed trait DynamicMod extends Mod
|
||||||
|
|
||||||
object Mod:
|
object Mod:
|
||||||
enum Attr extends Mod:
|
enum Attr extends Mod:
|
||||||
case Static(name: String, value: String, isJson: Boolean = false) extends Attr with StaticMod
|
case Static(name: String, value: String) extends Attr with StaticMod
|
||||||
case StaticValueAsPresence(name: String, value: Boolean) extends Attr with StaticMod
|
case StaticValueAsPresence(name: String, value: Boolean) extends Attr with StaticMod
|
||||||
|
case Binding(name: String, id: String, f: Map[String, String] => ?) extends Attr with StaticMod
|
||||||
|
case JsBinding(name: String, jsonValue: String, bindings: Map[String, ?])
|
||||||
|
extends Attr
|
||||||
|
with StaticMod
|
||||||
case Dyn(name: String, value: scalive.Dyn[String], isJson: Boolean = false)
|
case Dyn(name: String, value: scalive.Dyn[String], isJson: Boolean = false)
|
||||||
extends Attr
|
extends Attr
|
||||||
with DynamicMod
|
with DynamicMod
|
||||||
|
|
@ -96,7 +123,9 @@ object Mod:
|
||||||
extension (mod: Mod)
|
extension (mod: Mod)
|
||||||
private[scalive] def setAllUnchanged(): Unit =
|
private[scalive] def setAllUnchanged(): Unit =
|
||||||
mod match
|
mod match
|
||||||
case Attr.Static(_, _, _) => ()
|
case Attr.Static(_, _) => ()
|
||||||
|
case Attr.Binding(_, _, _) => ()
|
||||||
|
case Attr.JsBinding(_, _, _) => ()
|
||||||
case Attr.StaticValueAsPresence(_, _) => ()
|
case Attr.StaticValueAsPresence(_, _) => ()
|
||||||
case Attr.Dyn(_, value, _) => value.setUnchanged()
|
case Attr.Dyn(_, value, _) => value.setUnchanged()
|
||||||
case Attr.DynValueAsPresence(_, value) => value.setUnchanged()
|
case Attr.DynValueAsPresence(_, value) => value.setUnchanged()
|
||||||
|
|
@ -118,8 +147,10 @@ extension (mod: Mod)
|
||||||
|
|
||||||
private[scalive] def syncAll(): Unit =
|
private[scalive] def syncAll(): Unit =
|
||||||
mod match
|
mod match
|
||||||
case Attr.Static(_, _, _) => ()
|
case Attr.Static(_, _) => ()
|
||||||
case Attr.StaticValueAsPresence(_, _) => ()
|
case Attr.StaticValueAsPresence(_, _) => ()
|
||||||
|
case Attr.Binding(_, _, _) => ()
|
||||||
|
case Attr.JsBinding(_, _, _) => ()
|
||||||
case Attr.Dyn(_, value, _) => value.sync()
|
case Attr.Dyn(_, value, _) => value.sync()
|
||||||
case Attr.DynValueAsPresence(_, value) => value.sync()
|
case Attr.DynValueAsPresence(_, value) => value.sync()
|
||||||
case Content.Text(text) => ()
|
case Content.Text(text) => ()
|
||||||
|
|
@ -137,4 +168,25 @@ extension (mod: Mod)
|
||||||
case Content.DynSplit(v) =>
|
case Content.DynSplit(v) =>
|
||||||
v.sync()
|
v.sync()
|
||||||
v.callOnEveryChild(_.syncAll())
|
v.callOnEveryChild(_.syncAll())
|
||||||
|
|
||||||
|
private[scalive] def findBinding[Msg](id: String): Option[Map[String, String] => Msg] =
|
||||||
|
mod match
|
||||||
|
case Attr.Static(_, _) => None
|
||||||
|
case Attr.StaticValueAsPresence(_, _) => None
|
||||||
|
case Attr.Binding(_, eventId, f) =>
|
||||||
|
if id == eventId then Some(f.asInstanceOf[Map[String, String] => Msg])
|
||||||
|
else None
|
||||||
|
case Attr.JsBinding(_, _, bindings) =>
|
||||||
|
bindings.get(id).map(msg => _ => msg.asInstanceOf[Msg])
|
||||||
|
case Attr.Dyn(_, value, _) => None
|
||||||
|
case Attr.DynValueAsPresence(_, value) => None
|
||||||
|
case Content.Text(text) => None
|
||||||
|
case Content.Tag(el) => el.findBinding(id)
|
||||||
|
case Content.DynText(dyn) => None
|
||||||
|
case Content.DynElement(dyn) => dyn.currentValue.findBinding(id)
|
||||||
|
case Content.DynOptionElement(dyn) => dyn.currentValue.flatMap(_.findBinding(id))
|
||||||
|
case Content.DynElementColl(dyn) =>
|
||||||
|
dyn.currentValue.iterator.map(_.findBinding(id)).collectFirst { case Some(f) => f }
|
||||||
|
case Content.DynSplit(v) =>
|
||||||
|
v.currentValues.iterator.map(_.findBinding(id)).collectFirst { case Some(f) => f }
|
||||||
end extension
|
end extension
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,21 @@ package scalive
|
||||||
import zio.json.*
|
import zio.json.*
|
||||||
import zio.json.ast.Json
|
import zio.json.ast.Json
|
||||||
|
|
||||||
|
import java.util.Base64
|
||||||
|
import scala.util.Random
|
||||||
|
|
||||||
val JS: JSCommands.JSCommand = JSCommands.empty
|
val JS: JSCommands.JSCommand = JSCommands.empty
|
||||||
|
|
||||||
object JSCommands:
|
object JSCommands:
|
||||||
opaque type JSCommand = List[Json]
|
opaque type JSCommand = List[(Json, Option[Binding[?]])]
|
||||||
|
|
||||||
|
final case class Binding[Msg](id: String, msg: Msg)
|
||||||
|
|
||||||
def empty: JSCommand = List.empty
|
def empty: JSCommand = List.empty
|
||||||
|
|
||||||
object JSCommand:
|
object JSCommand:
|
||||||
given JsonEncoder[JSCommand] =
|
given JsonEncoder[JSCommand] =
|
||||||
JsonEncoder[Json].contramap(ops => Json.Arr(ops.reverse*))
|
JsonEncoder[Json].contramap(ops => Json.Arr(ops.map(_._1).reverse*))
|
||||||
|
|
||||||
private def classNames(names: String): Seq[String] = names.split("\\s+")
|
private def classNames(names: String): Seq[String] = names.split("\\s+")
|
||||||
private def transitionClasses(names: String | (String, String, String))
|
private def transitionClasses(names: String | (String, String, String))
|
||||||
|
|
@ -24,7 +29,15 @@ object JSCommands:
|
||||||
|
|
||||||
extension (ops: JSCommand)
|
extension (ops: JSCommand)
|
||||||
private def addOp[A: JsonEncoder](kind: String, args: A): JSCommand =
|
private def addOp[A: JsonEncoder](kind: String, args: A): JSCommand =
|
||||||
(kind, args).toJsonAST.fold(e => throw new IllegalArgumentException(e), identity) :: ops
|
(kind, args).toJsonAST.fold(e => throw new IllegalArgumentException(e), (_, None)) :: ops
|
||||||
|
|
||||||
|
private def addOp[A: JsonEncoder, Msg](kind: String, args: A, binding: Binding[Msg])
|
||||||
|
: JSCommand =
|
||||||
|
(kind, args).toJsonAST
|
||||||
|
.fold(e => throw new IllegalArgumentException(e), (_, Some(binding))) :: ops
|
||||||
|
|
||||||
|
private[scalive] def bindings: Map[String, ?] =
|
||||||
|
ops.flatMap(_._2).map(b => (b.id, b.msg)).toMap
|
||||||
|
|
||||||
def addClass = ClassOp("add_class", ops)
|
def addClass = ClassOp("add_class", ops)
|
||||||
def toggleClass = ClassOp("toggle_class", ops)
|
def toggleClass = ClassOp("toggle_class", ops)
|
||||||
|
|
@ -100,20 +113,22 @@ object JSCommands:
|
||||||
def popFocus() =
|
def popFocus() =
|
||||||
ops.addOp("pop_focus", Json.Obj.empty)
|
ops.addOp("pop_focus", Json.Obj.empty)
|
||||||
|
|
||||||
def push[A: JsonEncoder](
|
def push[Msg](
|
||||||
event: A,
|
event: Msg,
|
||||||
target: String = "",
|
target: String = "",
|
||||||
loading: String = "",
|
loading: String = "",
|
||||||
pageLoading: Boolean = false
|
pageLoading: Boolean = false
|
||||||
) =
|
) =
|
||||||
|
val bindingId = Base64.getUrlEncoder().withoutPadding().encodeToString(Random().nextBytes(12))
|
||||||
ops.addOp(
|
ops.addOp(
|
||||||
"push",
|
"push",
|
||||||
Args.Push(
|
Args.Push(
|
||||||
event.toJson,
|
bindingId,
|
||||||
Option.when(target.nonEmpty)(target),
|
Option.when(target.nonEmpty)(target),
|
||||||
Option.when(loading.nonEmpty)(loading),
|
Option.when(loading.nonEmpty)(loading),
|
||||||
Option.when(!pageLoading)(pageLoading)
|
Option.when(!pageLoading)(pageLoading)
|
||||||
)
|
),
|
||||||
|
Binding(bindingId, event)
|
||||||
)
|
)
|
||||||
|
|
||||||
def pushFocus(to: String = "") =
|
def pushFocus(to: String = "") =
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys:
|
||||||
new HtmlAttr(s"phx-$suffix", BooleanAsTrueFalseStringEncoder)
|
new HtmlAttr(s"phx-$suffix", BooleanAsTrueFalseStringEncoder)
|
||||||
private def phxAttrInt(suffix: String): HtmlAttr[Int] =
|
private def phxAttrInt(suffix: String): HtmlAttr[Int] =
|
||||||
new HtmlAttr(s"phx-$suffix", IntAsStringEncoder)
|
new HtmlAttr(s"phx-$suffix", IntAsStringEncoder)
|
||||||
private def phxAttrJson(suffix: String): HtmlAttrJsonValue =
|
private def phxAttrBinding(suffix: String): HtmlAttrBinding =
|
||||||
new HtmlAttrJsonValue(s"phx-$suffix")
|
new HtmlAttrBinding(s"phx-$suffix")
|
||||||
private def dataPhxAttr(suffix: String): HtmlAttr[String] =
|
private def dataPhxAttr(suffix: String): HtmlAttr[String] =
|
||||||
dataAttr(s"phx-$suffix")
|
dataAttr(s"phx-$suffix")
|
||||||
|
|
||||||
|
|
@ -33,41 +33,44 @@ package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys:
|
||||||
private[scalive] lazy val linkState = dataPhxAttr("link-state")
|
private[scalive] lazy val linkState = dataPhxAttr("link-state")
|
||||||
|
|
||||||
// Click
|
// Click
|
||||||
lazy val click = phxAttrJson("click")
|
lazy val onClick = phxAttrBinding("click")
|
||||||
lazy val clickAway = phxAttrJson("click-away")
|
lazy val onClickAway = phxAttrBinding("click-away")
|
||||||
|
|
||||||
// Focus
|
// Focus
|
||||||
lazy val blur = phxAttrJson("blur")
|
lazy val onBlur = phxAttrBinding("blur")
|
||||||
lazy val focus = phxAttrJson("focus")
|
lazy val onFocus = phxAttrBinding("focus")
|
||||||
lazy val windowBlur = phxAttrJson("window-blur")
|
lazy val onWindowBlur = phxAttrBinding("window-blur")
|
||||||
|
|
||||||
// Keyboard
|
// Keyboard
|
||||||
lazy val keydown = phxAttrJson("keydown")
|
lazy val onKeydown = phxAttrBinding("keydown")
|
||||||
lazy val keyup = phxAttrJson("keyup")
|
lazy val onKeyup = phxAttrBinding("keyup")
|
||||||
lazy val windowKeydown = phxAttrJson("window-keydown")
|
lazy val onWindowKeydown = phxAttrBinding("window-keydown")
|
||||||
lazy val windowKeyup = phxAttrJson("window-keyup")
|
lazy val onWindowKeyup = phxAttrBinding("window-keyup")
|
||||||
|
// For accepted values, see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
||||||
lazy val key = phxAttr("key")
|
lazy val key = phxAttr("key")
|
||||||
|
|
||||||
// Scroll
|
// Scroll
|
||||||
lazy val viewportTop = phxAttrJson("viewport-top")
|
lazy val onViewportTop = phxAttrBinding("viewport-top")
|
||||||
lazy val viewportBottom = phxAttrJson("viewport-bottom")
|
lazy val onViewportBottom = phxAttrBinding("viewport-bottom")
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
lazy val change = phxAttrJson("change")
|
lazy val onChange = phxAttrBinding("change")
|
||||||
lazy val submit = phxAttrJson("submit")
|
lazy val onSubmit = phxAttrBinding("submit")
|
||||||
lazy val autoRecover = phxAttrJson("auto-recover")
|
lazy val autoRecover = phxAttrBinding("auto-recover")
|
||||||
lazy val triggerAction = phxAttrBool("trigger-action")
|
lazy val triggerAction = phxAttrBool("trigger-action")
|
||||||
|
|
||||||
// Button
|
// Button
|
||||||
lazy val disableWith = phxAttr("disable-with")
|
lazy val disableWith = phxAttr("disable-with")
|
||||||
|
|
||||||
// Socket connection lifecycle
|
// Socket connection lifecycle
|
||||||
lazy val connected = phxAttrJson("connected")
|
lazy val onConnected = phxAttrBinding("connected")
|
||||||
lazy val disconnected = phxAttrJson("disconnected")
|
lazy val onDisconnected = phxAttrBinding("disconnected")
|
||||||
|
|
||||||
// DOM element lifecycle
|
// DOM element lifecycle
|
||||||
lazy val mounted = phxAttrJson("mounted")
|
lazy val onMounted = phxAttrBinding("mounted")
|
||||||
lazy val remove = phxAttrJson("remove")
|
lazy val onRemove = phxAttrBinding("remove")
|
||||||
lazy val update = new HtmlAttr["update" | "stream" | "ignore"](s"phx-update", Encoder(identity))
|
lazy val onUpdate =
|
||||||
|
new HtmlAttr["update" | "stream" | "ignore"](s"phx-update", Encoder(identity))
|
||||||
|
|
||||||
// Client hooks
|
// Client hooks
|
||||||
lazy val hook = phxAttr("hook")
|
lazy val hook = phxAttr("hook")
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ object StaticBuilder:
|
||||||
|
|
||||||
private def buildStaticFragments(el: HtmlElement): Seq[Option[String]] =
|
private def buildStaticFragments(el: HtmlElement): Seq[Option[String]] =
|
||||||
val attrs = el.attrMods.flatMap {
|
val attrs = el.attrMods.flatMap {
|
||||||
case Attr.Static(name, value, isJson) =>
|
case Attr.Static(name, value) => List(Some(s" $name='$value'"))
|
||||||
if isJson then List(Some(s" $name='$value'"))
|
|
||||||
else List(Some(s""" $name="$value""""))
|
|
||||||
case Attr.StaticValueAsPresence(name, value) => List(Some(s" $name"))
|
case Attr.StaticValueAsPresence(name, value) => List(Some(s" $name"))
|
||||||
|
case Attr.Binding(name, id, _) => List(Some(s""" $name="$id""""))
|
||||||
|
case Attr.JsBinding(name, json, _) => List(Some(s" $name='$json'"))
|
||||||
case Attr.Dyn(name, value, isJson) =>
|
case Attr.Dyn(name, value, isJson) =>
|
||||||
if isJson then List(Some(s" $name='"), None, Some("'"))
|
if isJson then List(Some(s" $name='"), None, Some("'"))
|
||||||
else List(Some(s""" $name=""""), None, Some('"'.toString))
|
else List(Some(s""" $name=""""), None, Some('"'.toString))
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ lazy val DoubleAsIsEncoder: Encoder[Double, Double] = AsIsEncoder()
|
||||||
lazy val DoubleAsStringEncoder: Encoder[Double, String] =
|
lazy val DoubleAsStringEncoder: Encoder[Double, String] =
|
||||||
Encoder[Double, String](_.toString)
|
Encoder[Double, String](_.toString)
|
||||||
|
|
||||||
|
lazy val BooleanAsStringEncoder: Encoder[Boolean, String] =
|
||||||
|
Encoder[Boolean, String](_.toString)
|
||||||
|
|
||||||
val BooleanAsIsEncoder: Encoder[Boolean, Boolean] = AsIsEncoder()
|
val BooleanAsIsEncoder: Encoder[Boolean, Boolean] = AsIsEncoder()
|
||||||
|
|
||||||
lazy val BooleanAsAttrPresenceEncoder: Encoder[Boolean, String] =
|
lazy val BooleanAsAttrPresenceEncoder: Encoder[Boolean, String] =
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import CounterLiveView.*
|
||||||
import monocle.syntax.all.*
|
import monocle.syntax.all.*
|
||||||
import scalive.*
|
import scalive.*
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.json.*
|
|
||||||
import zio.stream.ZStream
|
import zio.stream.ZStream
|
||||||
|
|
||||||
class CounterLiveView() extends LiveView[Msg, Model]:
|
class CounterLiveView() extends LiveView[Msg, Model]:
|
||||||
|
|
@ -35,7 +34,7 @@ class CounterLiveView() extends LiveView[Msg, Model]:
|
||||||
cls := "flex flex-wrap items-center gap-3",
|
cls := "flex flex-wrap items-center gap-3",
|
||||||
button(
|
button(
|
||||||
cls := "btn btn-default",
|
cls := "btn btn-default",
|
||||||
phx.click := Msg.ToggleCounter,
|
phx.onClick(Msg.ToggleCounter),
|
||||||
model(_.isVisible match
|
model(_.isVisible match
|
||||||
case true => "Hide counter"
|
case true => "Hide counter"
|
||||||
case false => "Show counter")
|
case false => "Show counter")
|
||||||
|
|
@ -46,7 +45,7 @@ class CounterLiveView() extends LiveView[Msg, Model]:
|
||||||
cls := "flex items-center justify-center gap-4",
|
cls := "flex items-center justify-center gap-4",
|
||||||
button(
|
button(
|
||||||
cls := "btn btn-neutral",
|
cls := "btn btn-neutral",
|
||||||
phx.click := Msg.DecCounter,
|
phx.onClick(Msg.DecCounter),
|
||||||
"-"
|
"-"
|
||||||
),
|
),
|
||||||
div(
|
div(
|
||||||
|
|
@ -55,7 +54,7 @@ class CounterLiveView() extends LiveView[Msg, Model]:
|
||||||
),
|
),
|
||||||
button(
|
button(
|
||||||
cls := "btn btn-neutral",
|
cls := "btn btn-neutral",
|
||||||
phx.click := Msg.IncCounter,
|
phx.onClick(Msg.IncCounter),
|
||||||
"+"
|
"+"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -70,7 +69,7 @@ end CounterLiveView
|
||||||
|
|
||||||
object CounterLiveView:
|
object CounterLiveView:
|
||||||
|
|
||||||
enum Msg derives JsonCodec:
|
enum Msg:
|
||||||
case ToggleCounter
|
case ToggleCounter
|
||||||
case IncCounter
|
case IncCounter
|
||||||
case DecCounter
|
case DecCounter
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ object Example extends ZIOAppDefault:
|
||||||
(_, req) =>
|
(_, req) =>
|
||||||
val q = req.queryParam("q").map("Param : " ++ _).getOrElse("No param")
|
val q = req.queryParam("q").map("Param : " ++ _).getOrElse("No param")
|
||||||
ListLiveView(q)
|
ListLiveView(q)
|
||||||
|
),
|
||||||
|
LiveRoute(
|
||||||
|
Root / "todo",
|
||||||
|
(_, _) => TodoLiveView()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ import zio.stream.ZStream
|
||||||
class HomeLiveView() extends LiveView[String, Unit]:
|
class HomeLiveView() extends LiveView[String, Unit]:
|
||||||
val links = List(
|
val links = List(
|
||||||
"/counter" -> "Counter",
|
"/counter" -> "Counter",
|
||||||
"/list?q=test" -> "List"
|
"/list?q=test" -> "List",
|
||||||
|
"/todo" -> "Todo"
|
||||||
)
|
)
|
||||||
|
|
||||||
def init = ZIO.succeed(())
|
def init = ZIO.succeed(())
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import ListLiveView.*
|
||||||
import monocle.syntax.all.*
|
import monocle.syntax.all.*
|
||||||
import scalive.*
|
import scalive.*
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.json.*
|
|
||||||
import zio.stream.ZStream
|
import zio.stream.ZStream
|
||||||
|
|
||||||
class ListLiveView(someParam: String) extends LiveView[Msg, Model]:
|
class ListLiveView(someParam: String) extends LiveView[Msg, Model]:
|
||||||
|
|
@ -49,13 +48,13 @@ class ListLiveView(someParam: String) extends LiveView[Msg, Model]:
|
||||||
cls := "card-actions",
|
cls := "card-actions",
|
||||||
button(
|
button(
|
||||||
cls := "btn btn-default",
|
cls := "btn btn-default",
|
||||||
phx.click := Msg.IncAge(1),
|
phx.onClick(Msg.IncAge(1)),
|
||||||
"Inc age"
|
"Inc age"
|
||||||
),
|
),
|
||||||
span(cls := "grow"),
|
span(cls := "grow"),
|
||||||
button(
|
button(
|
||||||
cls := "btn btn-neutral",
|
cls := "btn btn-neutral",
|
||||||
phx.click := JS.toggleClass("btn-neutral btn-accent").push(Msg.IncAge(-5)),
|
phx.onClick(JS.toggleClass("btn-neutral btn-accent").push(Msg.IncAge(-5))),
|
||||||
"Toggle color"
|
"Toggle color"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -68,7 +67,7 @@ end ListLiveView
|
||||||
|
|
||||||
object ListLiveView:
|
object ListLiveView:
|
||||||
|
|
||||||
enum Msg derives JsonCodec:
|
enum Msg:
|
||||||
case IncAge(value: Int)
|
case IncAge(value: Int)
|
||||||
|
|
||||||
final case class Model(
|
final case class Model(
|
||||||
|
|
|
||||||
88
example/src/TodoLiveView.scala
Normal file
88
example/src/TodoLiveView.scala
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import TodoLiveView.*
|
||||||
|
import monocle.syntax.all.*
|
||||||
|
import scalive.*
|
||||||
|
import zio.*
|
||||||
|
import zio.stream.ZStream
|
||||||
|
|
||||||
|
class TodoLiveView() extends LiveView[Msg, Model]:
|
||||||
|
|
||||||
|
def init = ZIO.succeed(Model(List(Todo(99, "some task"))))
|
||||||
|
|
||||||
|
def update(model: Model) =
|
||||||
|
case Msg.Add(text) =>
|
||||||
|
val nextId = model.todos.maxByOption(_.id).map(_.id).getOrElse(1)
|
||||||
|
ZIO.succeed(
|
||||||
|
model
|
||||||
|
.focus(_.todos)
|
||||||
|
.modify(_.appended(Todo(nextId, text)))
|
||||||
|
)
|
||||||
|
case Msg.Remove(id) =>
|
||||||
|
ZIO.succeed(
|
||||||
|
model
|
||||||
|
.focus(_.todos)
|
||||||
|
.modify(_.filterNot(_.id == id))
|
||||||
|
)
|
||||||
|
case Msg.ToggleCompletion(id) =>
|
||||||
|
ZIO.succeed(
|
||||||
|
model
|
||||||
|
.focus(_.todos)
|
||||||
|
.modify(
|
||||||
|
_.map(todo => if todo.id == id then todo.copy(completed = todo.completed) else todo)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def view(model: Dyn[Model]) =
|
||||||
|
div(
|
||||||
|
cls := "mx-auto card bg-base-100 max-w-2xl shadow-xl space-y-6 p-6",
|
||||||
|
div(
|
||||||
|
cls := "card-body",
|
||||||
|
h1(cls := "card-title", "Todos"),
|
||||||
|
div(
|
||||||
|
cls := "flex items-center gap-3",
|
||||||
|
input(
|
||||||
|
cls := "input input-bordered grow",
|
||||||
|
typ := "text",
|
||||||
|
nameAttr := "todo-text",
|
||||||
|
placeholder := "What needs to be done?",
|
||||||
|
phx.onKeyup.withValue(Msg.Add(_)),
|
||||||
|
phx.key := "Enter",
|
||||||
|
phx.value("test") := "some value"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
form(
|
||||||
|
cls := "flex items-center gap-3",
|
||||||
|
phx.onChange(p => Msg.Add(p("todo-text"))),
|
||||||
|
phx.onSubmit(p => Msg.Add(p("todo-text"))),
|
||||||
|
input(
|
||||||
|
cls := "input input-bordered grow",
|
||||||
|
typ := "text",
|
||||||
|
nameAttr := "todo-text",
|
||||||
|
placeholder := "What needs to be done?"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
ul(
|
||||||
|
cls := "divide-y divide-base-200",
|
||||||
|
model(_.todos).splitByIndex((_, elem) =>
|
||||||
|
li(
|
||||||
|
cls := "py-3 flex flex-wrap items-center justify-between gap-2",
|
||||||
|
elem(_.text)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscriptions(model: Model) = ZStream.empty
|
||||||
|
end TodoLiveView
|
||||||
|
|
||||||
|
object TodoLiveView:
|
||||||
|
|
||||||
|
enum Msg:
|
||||||
|
case Add(text: String)
|
||||||
|
case Remove(id: Int)
|
||||||
|
case ToggleCompletion(id: Int)
|
||||||
|
|
||||||
|
final case class Model(todos: List[Todo] = List.empty, filter: Filter = Filter.All)
|
||||||
|
final case class Todo(id: Int, text: String, completed: Boolean = false)
|
||||||
|
enum Filter:
|
||||||
|
case All, Active, Completed
|
||||||
|
|
@ -14,10 +14,9 @@ import zio.stream.ZStream
|
||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
import scala.util.Random
|
import scala.util.Random
|
||||||
|
|
||||||
final case class LiveRoute[A, Msg: JsonCodec, Model](
|
final case class LiveRoute[A, Msg, Model](
|
||||||
path: PathCodec[A],
|
path: PathCodec[A],
|
||||||
liveviewBuilder: (A, Request) => LiveView[Msg, Model]):
|
liveviewBuilder: (A, Request) => LiveView[Msg, Model]):
|
||||||
val messageCodec = JsonCodec[Msg]
|
|
||||||
|
|
||||||
def toZioRoute(rootLayout: HtmlElement => HtmlElement): Route[Any, Throwable] =
|
def toZioRoute(rootLayout: HtmlElement => HtmlElement): Route[Any, Throwable] =
|
||||||
Method.GET / path -> handler { (params: A, req: Request) =>
|
Method.GET / path -> handler { (params: A, req: Request) =>
|
||||||
|
|
@ -44,7 +43,6 @@ final case class LiveRoute[A, Msg: JsonCodec, Model](
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
end LiveRoute
|
|
||||||
|
|
||||||
class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?, ?]]]):
|
class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?, ?]]]):
|
||||||
def diffsStream: ZStream[Any, Nothing, (Payload, Meta)] =
|
def diffsStream: ZStream[Any, Nothing, (Payload, Meta)] =
|
||||||
|
|
@ -54,7 +52,7 @@ class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?, ?]]
|
||||||
.mergeAllUnbounded()(m.values.map(_.outbox).toList*)
|
.mergeAllUnbounded()(m.values.map(_.outbox).toList*)
|
||||||
).flatMapParSwitch(1)(identity)
|
).flatMapParSwitch(1)(identity)
|
||||||
|
|
||||||
def join[Msg: JsonCodec, Model](
|
def join[Msg, Model](
|
||||||
id: String,
|
id: String,
|
||||||
token: String,
|
token: String,
|
||||||
lv: LiveView[Msg, Model],
|
lv: LiveView[Msg, Model],
|
||||||
|
|
@ -86,17 +84,11 @@ class LiveChannel(private val sockets: SubscriptionRef[Map[String, Socket[?, ?]]
|
||||||
ZIO.logWarning(s"Tried to leave LiveView $id which doesn't exist").as(m)
|
ZIO.logWarning(s"Tried to leave LiveView $id which doesn't exist").as(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
def event(id: String, value: String, meta: WebSocketMessage.Meta): UIO[Unit] =
|
def event(id: String, event: Payload.Event, meta: WebSocketMessage.Meta): UIO[Unit] =
|
||||||
sockets.get.flatMap { m =>
|
sockets.get.flatMap { m =>
|
||||||
m.get(id) match
|
m.get(id) match
|
||||||
case Some(socket) =>
|
case Some(socket) =>
|
||||||
socket.inbox
|
socket.inbox.offer(event -> meta).unit
|
||||||
.offer(
|
|
||||||
value
|
|
||||||
.fromJson(using socket.messageCodec.decoder)
|
|
||||||
.getOrElse(throw new IllegalArgumentException())
|
|
||||||
-> meta
|
|
||||||
).unit
|
|
||||||
case None => ZIO.unit
|
case None => ZIO.unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,9 +165,7 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo
|
||||||
.map(route.liveviewBuilder(_, req))
|
.map(route.liveviewBuilder(_, req))
|
||||||
.map(
|
.map(
|
||||||
ZIO.logDebug(s"Joining LiveView ${route.path.toString} ${message.topic}") *>
|
ZIO.logDebug(s"Joining LiveView ${route.path.toString} ${message.topic}") *>
|
||||||
liveChannel.join(message.topic, session, _, message.meta)(
|
liveChannel.join(message.topic, session, _, message.meta)
|
||||||
using route.messageCodec
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.collectFirst { case Some(join) => join.map(_ => None) }
|
.collectFirst { case Some(join) => join.map(_ => None) }
|
||||||
|
|
@ -185,7 +175,7 @@ class LiveRouter(rootLayout: HtmlElement => HtmlElement, liveRoutes: List[LiveRo
|
||||||
liveChannel
|
liveChannel
|
||||||
.leave(message.topic)
|
.leave(message.topic)
|
||||||
.as(Some(message.okReply))
|
.as(Some(message.okReply))
|
||||||
case Payload.Event(_, event, _) =>
|
case event: Payload.Event =>
|
||||||
liveChannel
|
liveChannel
|
||||||
.event(message.topic, event, message.meta)
|
.event(message.topic, event, message.meta)
|
||||||
.map(_ => None)
|
.map(_ => None)
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,21 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
import scalive.WebSocketMessage.LiveResponse
|
import scalive.WebSocketMessage.LiveResponse
|
||||||
|
import scalive.WebSocketMessage.Payload
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.Queue
|
import zio.Queue
|
||||||
import zio.json.*
|
|
||||||
import zio.stream.ZStream
|
|
||||||
import zio.stream.SubscriptionRef
|
import zio.stream.SubscriptionRef
|
||||||
import scalive.WebSocketMessage.Payload
|
import zio.stream.ZStream
|
||||||
|
|
||||||
final case class Socket[Msg: JsonCodec, Model] private (
|
final case class Socket[Msg, Model] private (
|
||||||
id: String,
|
id: String,
|
||||||
token: String,
|
token: String,
|
||||||
inbox: Queue[(Msg, WebSocketMessage.Meta)],
|
inbox: Queue[(Payload.Event, WebSocketMessage.Meta)],
|
||||||
outbox: ZStream[Any, Nothing, (Payload, WebSocketMessage.Meta)],
|
outbox: ZStream[Any, Nothing, (Payload, WebSocketMessage.Meta)],
|
||||||
shutdown: UIO[Unit]):
|
shutdown: UIO[Unit])
|
||||||
val messageCodec = JsonCodec[Msg]
|
|
||||||
|
|
||||||
object Socket:
|
object Socket:
|
||||||
def start[Msg: JsonCodec, Model](
|
def start[Msg, Model](
|
||||||
id: String,
|
id: String,
|
||||||
token: String,
|
token: String,
|
||||||
lv: LiveView[Msg, Model],
|
lv: LiveView[Msg, Model],
|
||||||
|
|
@ -25,7 +23,7 @@ object Socket:
|
||||||
): RIO[Scope, Socket[Msg, Model]] =
|
): RIO[Scope, Socket[Msg, Model]] =
|
||||||
ZIO.logAnnotate("lv", id) {
|
ZIO.logAnnotate("lv", id) {
|
||||||
for
|
for
|
||||||
inbox <- Queue.bounded[(Msg, WebSocketMessage.Meta)](4)
|
inbox <- Queue.bounded[(Payload.Event, WebSocketMessage.Meta)](4)
|
||||||
outHub <- Hub.unbounded[(Payload, WebSocketMessage.Meta)]
|
outHub <- Hub.unbounded[(Payload, WebSocketMessage.Meta)]
|
||||||
|
|
||||||
initModel <- lv.init
|
initModel <- lv.init
|
||||||
|
|
@ -42,10 +40,18 @@ object Socket:
|
||||||
.flatMapParSwitch(1, 1)(identity)
|
.flatMapParSwitch(1, 1)(identity)
|
||||||
.map(_ -> meta.copy(messageRef = None, eventType = "diff"))
|
.map(_ -> meta.copy(messageRef = None, eventType = "diff"))
|
||||||
|
|
||||||
clientFiber <- clientMsgStream.runForeach { (msg, meta) =>
|
clientFiber <- clientMsgStream.runForeach { (event, meta) =>
|
||||||
for
|
for
|
||||||
(modelVar, el) <- ref.get
|
(modelVar, el) <- ref.get
|
||||||
updatedModel <- lv.update(modelVar.currentValue)(msg)
|
f <-
|
||||||
|
ZIO
|
||||||
|
.succeed(el.findBinding(event.event))
|
||||||
|
.someOrFail(
|
||||||
|
new IllegalArgumentException(
|
||||||
|
s"No binding found for event ID ${event.event}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
updatedModel <- lv.update(modelVar.currentValue)(f(event.params))
|
||||||
_ = modelVar.set(updatedModel)
|
_ = modelVar.set(updatedModel)
|
||||||
_ <- lvStreamRef.set(lv.subscriptions(updatedModel))
|
_ <- lvStreamRef.set(lv.subscriptions(updatedModel))
|
||||||
diff = el.diff()
|
diff = el.diff()
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ package scalive
|
||||||
|
|
||||||
import scalive.WebSocketMessage.LiveResponse
|
import scalive.WebSocketMessage.LiveResponse
|
||||||
import scalive.WebSocketMessage.Payload
|
import scalive.WebSocketMessage.Payload
|
||||||
import scalive.WebSocketMessage.Payload.EventType
|
|
||||||
import zio.Chunk
|
import zio.Chunk
|
||||||
|
import zio.http.QueryParams
|
||||||
import zio.json.*
|
import zio.json.*
|
||||||
import zio.json.ast.Json
|
import zio.json.ast.Json
|
||||||
|
|
||||||
|
|
@ -87,29 +87,28 @@ object WebSocketMessage:
|
||||||
case Close
|
case Close
|
||||||
case Reply(status: String, response: LiveResponse)
|
case Reply(status: String, response: LiveResponse)
|
||||||
case Diff(diff: scalive.Diff)
|
case Diff(diff: scalive.Diff)
|
||||||
case Event(`type`: Payload.EventType, event: String, value: Map[String, String])
|
case Event(`type`: String, event: String, value: Json)
|
||||||
|
|
||||||
object Payload:
|
object Payload:
|
||||||
given JsonCodec[Payload.Join] = JsonCodec.derived
|
given JsonCodec[Payload.Join] = JsonCodec.derived
|
||||||
given JsonEncoder[Payload.Reply] = JsonEncoder.derived
|
given JsonEncoder[Payload.Reply] = JsonEncoder.derived
|
||||||
given JsonCodec[Payload.Event] = JsonCodec.derived
|
given JsonCodec[Payload.Event] = JsonCodec.derived
|
||||||
given JsonEncoder[Payload.Diff] = JsonEncoder[scalive.Diff].contramap(_.diff)
|
given JsonEncoder[Payload.Diff] = JsonEncoder[scalive.Diff].contramap(_.diff)
|
||||||
|
|
||||||
|
extension (p: Payload.Event)
|
||||||
|
def params: Map[String, String] =
|
||||||
|
p.`type` match
|
||||||
|
case "form" =>
|
||||||
|
QueryParams
|
||||||
|
.decode(
|
||||||
|
p.value.asString.getOrElse(throw new IllegalArgumentException())
|
||||||
|
).map.view.mapValues(_.head).toMap
|
||||||
|
|
||||||
|
case _ => p.value.as[Map[String, String]].getOrElse(throw new IllegalArgumentException())
|
||||||
|
|
||||||
def okReply(response: LiveResponse) =
|
def okReply(response: LiveResponse) =
|
||||||
Payload.Reply("ok", response)
|
Payload.Reply("ok", response)
|
||||||
|
|
||||||
enum EventType:
|
|
||||||
case Click
|
|
||||||
object EventType:
|
|
||||||
given JsonCodec[EventType] = JsonCodec[String].transformOrFail(
|
|
||||||
{
|
|
||||||
case "click" => Right(Click)
|
|
||||||
case s => Left(s"Unsupported event type: $s")
|
|
||||||
},
|
|
||||||
{ case Click =>
|
|
||||||
"click"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
enum LiveResponse:
|
enum LiveResponse:
|
||||||
case Empty
|
case Empty
|
||||||
case InitDiff(rendered: scalive.Diff)
|
case InitDiff(rendered: scalive.Diff)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue