Improve LiveView's API with inspiration from TEA

This commit is contained in:
Paul-Henri Froidmont 2025-09-03 22:38:52 +02:00
parent 4af9a78408
commit 08036ab5aa
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
10 changed files with 254 additions and 238 deletions

View file

@ -2,28 +2,23 @@ package scalive
package playground
import scalive.*
import zio.*
final case class MyModel(
cls: String = "text-xs",
bool: Boolean = true,
elems: List[Elem] = List.empty)
final case class Elem(name: String, age: Int)
import TestView.*
class TestView extends LiveView[Msg, Model]:
class TestView(initialModel: MyModel) extends LiveView[TestView.Event]:
import TestView.Event.*
def init = ZIO.succeed(Model())
private val modelVar = Var[MyModel](initialModel)
def update(model: Model) =
case Msg.UpdateModel(f) => ZIO.succeed(f(model))
def handleEvent =
case UpdateModel(f) => modelVar.update(f)
val el: HtmlElement =
def view(model: Dyn[Model]) =
div(
idAttr := "42",
cls := modelVar(_.cls),
disabled := modelVar(_.bool),
cls := model(_.cls),
disabled := model(_.bool),
ul(
modelVar(_.elems).splitByIndex((_, elem) =>
model(_.elems).splitByIndex((_, elem) =>
li(
"Nom: ",
elem(_.name),
@ -35,5 +30,12 @@ class TestView(initialModel: MyModel) extends LiveView[TestView.Event]:
)
object TestView:
enum Event:
case UpdateModel(f: MyModel => MyModel)
enum Msg:
case UpdateModel(f: Model => Model)
final case class Model(
cls: String = "text-xs",
bool: Boolean = true,
elems: List[Elem] = List.empty)
final case class Elem(name: String, age: Int)

View file

@ -4,87 +4,78 @@ package playground
import scalive.*
import zio.json.*
extension (lv: LiveView[?])
def renderHtml: String =
HtmlBuilder.build(lv.el)
extension (el: HtmlElement) def html: String = HtmlBuilder.build(el)
import TestView.*
@main
def main =
val initModel = MyModel(elems =
val initModel = Model(elems =
List(
Elem("a", 10),
Elem("b", 15),
Elem("c", 30)
)
)
val lv = TestView(initModel)
val modelVar = Var(initModel)
val lv = TestView()
val el = lv.view(modelVar)
println("Init")
println(lv.renderHtml)
println(lv.diff().toJsonPretty)
println(el.html)
println(el.diff().toJsonPretty)
println("Edit class attribue")
lv.handleEvent(
TestView.Event.UpdateModel(_.copy(cls = "text-lg"))
)
println(lv.diff().toJsonPretty)
modelVar.update(_.copy(cls = "text-lg"))
println(el.diff().toJsonPretty)
println("Edit first and last")
lv.handleEvent(
TestView.Event.UpdateModel(
_.copy(elems =
List(
Elem("x", 10),
Elem("b", 15),
Elem("c", 99)
)
modelVar.update(
_.copy(elems =
List(
Elem("x", 10),
Elem("b", 15),
Elem("c", 99)
)
)
)
println(lv.diff().toJsonPretty)
println(lv.diff().toJsonPretty)
println(el.diff().toJsonPretty)
println(el.diff().toJsonPretty)
println("Add one")
lv.handleEvent(
TestView.Event.UpdateModel(
_.copy(elems =
List(
Elem("x", 10),
Elem("b", 15),
Elem("c", 99),
Elem("d", 35)
)
modelVar.update(
_.copy(elems =
List(
Elem("x", 10),
Elem("b", 15),
Elem("c", 99),
Elem("d", 35)
)
)
)
println(lv.diff().toJsonPretty)
println(lv.renderHtml)
println(el.diff().toJsonPretty)
println(el.html)
println("Remove first")
lv.handleEvent(
TestView.Event.UpdateModel(
_.copy(elems =
List(
Elem("b", 15),
Elem("c", 99),
Elem("d", 35)
)
modelVar.update(
_.copy(elems =
List(
Elem("b", 15),
Elem("c", 99),
Elem("d", 35)
)
)
)
println(lv.diff().toJsonPretty)
println(lv.renderHtml)
println(el.diff().toJsonPretty)
println(el.html)
println("Remove all")
lv.handleEvent(
TestView.Event.UpdateModel(
_.copy(
cls = "text-lg",
bool = false,
elems = List.empty
)
modelVar.update(
_.copy(
cls = "text-lg",
bool = false,
elems = List.empty
)
)
println(lv.diff().toJsonPretty)
println(lv.diff().toJsonPretty)
println(lv.renderHtml)
println(el.diff().toJsonPretty)
println(el.diff().toJsonPretty)
println(el.html)
end main

View file

@ -46,5 +46,5 @@ object Diff:
case Diff.Value(value) => Json.Str(value)
case Diff.Dynamic(index, diff) =>
Json.Obj(index.toString -> toJson(diff))
case Diff.Deleted => Json.Bool(false)
case Diff.Deleted => Json.Str("")
end Diff

View file

@ -46,7 +46,7 @@ extension [T](parent: Dyn[List[T]])
)
)
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 changed: Boolean = true
def set(value: T): Unit =
@ -61,10 +61,10 @@ class Var[T] private (initial: T) extends Dyn[T]:
private[scalive] def setUnchanged(): Unit = changed = false
private[scalive] inline def sync(): Unit = ()
private[scalive] def callOnEveryChild(f: T => Unit): Unit = f(currentValue)
object Var:
private object Var:
def apply[T](initial: T): Var[T] = new Var(initial)
class DerivedVar[I, O] private[scalive] (parent: Var[I], f: I => O) extends Dyn[O]:
private class DerivedVar[I, O] private[scalive] (parent: Var[I], f: I => O) extends Dyn[O]:
private[scalive] var currentValue: O = f(parent.currentValue)
private[scalive] var changed: Boolean = true
@ -88,7 +88,7 @@ class DerivedVar[I, O] private[scalive] (parent: Var[I], f: I => O) extends Dyn[
private[scalive] def callOnEveryChild(f: O => Unit): Unit = f(currentValue)
class SplitVar[I, O, Key](
private class SplitVar[I, O, Key](
parent: Dyn[List[I]],
key: I => Key,
project: (Key, Dyn[I]) => O):

View file

@ -22,12 +22,18 @@ class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
mods.collect { case mod: (Mod.Attr & DynamicMod) => mod }
def dynamicContentMods: Seq[Mod.Content & DynamicMod] =
mods.collect { case mod: (Mod.Content & DynamicMod) => mod }
private[scalive] def syncAll(): Unit = mods.foreach(_.syncAll())
private[scalive] def setAllUnchanged(): Unit = dynamicMods.foreach(_.setAllUnchanged())
def prepended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.prependedAll(mod))
def apended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.appendedAll(mod))
private[scalive] def syncAll(): Unit = mods.foreach(_.syncAll())
private[scalive] def setAllUnchanged(): Unit = dynamicMods.foreach(_.setAllUnchanged())
private[scalive] def diff(trackUpdates: Boolean = true): Diff =
syncAll()
val diff = DiffBuilder.build(this, trackUpdates = trackUpdates)
setAllUnchanged()
diff
class HtmlTag(val name: String, val void: Boolean = false):
def apply(mods: Mod*): HtmlElement = HtmlElement(this, mods.toVector)

View file

@ -1,11 +1,9 @@
package scalive
trait LiveView[Event]:
def handleEvent: Event => Unit
val el: HtmlElement
import zio.*
private[scalive] def diff(trackUpdates: Boolean = true): Diff =
el.syncAll()
val diff = DiffBuilder.build(el, trackUpdates = trackUpdates)
el.setAllUnchanged()
diff
trait LiveView[Msg, Model]:
def init: Task[Model]
def update(model: Model): Msg => Task[Model]
def view(model: Dyn[Model]): HtmlElement
// def subscriptions(model: Model): ZStream[Any, Nothing, Msg]