mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Improve LiveView's API with inspiration from TEA
This commit is contained in:
parent
4af9a78408
commit
08036ab5aa
10 changed files with 254 additions and 238 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ object LiveViewSpec extends TestSuite:
|
|||
cls: String = "text-sm",
|
||||
items: List[NestedModel] = List.empty)
|
||||
final case class NestedModel(name: String, age: Int)
|
||||
final case class UpdateEvent(f: TestModel => TestModel)
|
||||
|
||||
def assertEqualsDiff(el: HtmlElement, expected: Json, trackChanges: Boolean = true) =
|
||||
el.syncAll()
|
||||
|
|
@ -26,15 +25,12 @@ object LiveViewSpec extends TestSuite:
|
|||
val tests = Tests {
|
||||
|
||||
test("Static only") {
|
||||
val lv =
|
||||
new LiveView[Nothing]:
|
||||
val el = div("Static string")
|
||||
def handleEvent = _ => ()
|
||||
lv.el.syncAll()
|
||||
val el = div("Static string")
|
||||
el.syncAll()
|
||||
|
||||
test("init") {
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj(
|
||||
"s" -> Json.Arr(Json.Str("<div>Static string</div>"))
|
||||
),
|
||||
|
|
@ -42,27 +38,24 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff") {
|
||||
assertEqualsDiff(lv.el, emptyDiff)
|
||||
assertEqualsDiff(el, emptyDiff)
|
||||
}
|
||||
}
|
||||
|
||||
test("Dynamic string") {
|
||||
val lv =
|
||||
new LiveView[UpdateEvent]:
|
||||
val model = Var(TestModel())
|
||||
val el =
|
||||
div(
|
||||
h1(model(_.title)),
|
||||
p(model(_.otherString))
|
||||
)
|
||||
def handleEvent = evt => model.update(evt.f)
|
||||
val model = Var(TestModel())
|
||||
val el =
|
||||
div(
|
||||
h1(model(_.title)),
|
||||
p(model(_.otherString))
|
||||
)
|
||||
|
||||
lv.el.syncAll()
|
||||
lv.el.setAllUnchanged()
|
||||
el.syncAll()
|
||||
el.setAllUnchanged()
|
||||
|
||||
test("init") {
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json
|
||||
.Obj(
|
||||
"s" -> Json.Arr(Json.Str("<div><h1>"), Json.Str("</h1><p>"), Json.Str("</p></div>")),
|
||||
|
|
@ -73,24 +66,24 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff no update") {
|
||||
assertEqualsDiff(lv.el, emptyDiff)
|
||||
assertEqualsDiff(el, emptyDiff)
|
||||
}
|
||||
test("diff with update") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(title = "title updated")))
|
||||
model.update(_.copy(title = "title updated"))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj("0" -> Json.Str("title updated"))
|
||||
)
|
||||
}
|
||||
test("diff with update and no change") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(title = "title value")))
|
||||
assertEqualsDiff(lv.el, emptyDiff)
|
||||
model.update(_.copy(title = "title value"))
|
||||
assertEqualsDiff(el, emptyDiff)
|
||||
}
|
||||
test("diff with update in multiple commands") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(title = "title updated")))
|
||||
lv.handleEvent(UpdateEvent(_.copy(otherString = "other string updated")))
|
||||
model.update(_.copy(title = "title updated"))
|
||||
model.update(_.copy(otherString = "other string updated"))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json
|
||||
.Obj(
|
||||
"0" -> Json.Str("title updated"),
|
||||
|
|
@ -101,19 +94,16 @@ object LiveViewSpec extends TestSuite:
|
|||
}
|
||||
|
||||
test("Dynamic attribute") {
|
||||
val lv =
|
||||
new LiveView[UpdateEvent]:
|
||||
val model = Var(TestModel())
|
||||
val el =
|
||||
div(cls := model(_.cls))
|
||||
def handleEvent = evt => model.update(evt.f)
|
||||
val model = Var(TestModel())
|
||||
val el =
|
||||
div(cls := model(_.cls))
|
||||
|
||||
lv.el.syncAll()
|
||||
lv.el.setAllUnchanged()
|
||||
el.syncAll()
|
||||
el.setAllUnchanged()
|
||||
|
||||
test("init") {
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json
|
||||
.Obj(
|
||||
"s" -> Json
|
||||
|
|
@ -124,35 +114,32 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff no update") {
|
||||
assertEqualsDiff(lv.el, emptyDiff)
|
||||
assertEqualsDiff(el, emptyDiff)
|
||||
}
|
||||
test("diff with update") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(cls = "text-md")))
|
||||
model.update(_.copy(cls = "text-md"))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj("0" -> Json.Str("text-md"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
test("when mod") {
|
||||
val lv =
|
||||
new LiveView[UpdateEvent]:
|
||||
val model = Var(TestModel())
|
||||
val el =
|
||||
div(
|
||||
model.when(_.bool)(
|
||||
div("static string", model(_.nestedTitle))
|
||||
)
|
||||
)
|
||||
def handleEvent = evt => model.update(evt.f)
|
||||
val model = Var(TestModel())
|
||||
val el =
|
||||
div(
|
||||
model.when(_.bool)(
|
||||
div("static string", model(_.nestedTitle))
|
||||
)
|
||||
)
|
||||
|
||||
lv.el.syncAll()
|
||||
lv.el.setAllUnchanged()
|
||||
el.syncAll()
|
||||
el.setAllUnchanged()
|
||||
|
||||
test("init") {
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json
|
||||
.Obj(
|
||||
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
||||
|
|
@ -162,16 +149,16 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff no update") {
|
||||
assertEqualsDiff(lv.el, emptyDiff)
|
||||
assertEqualsDiff(el, emptyDiff)
|
||||
}
|
||||
test("diff with unrelated update") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(title = "title updated")))
|
||||
assertEqualsDiff(lv.el, emptyDiff)
|
||||
model.update(_.copy(title = "title updated"))
|
||||
assertEqualsDiff(el, emptyDiff)
|
||||
}
|
||||
test("diff when true and nested update") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(bool = true)))
|
||||
model.update(_.copy(bool = true))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
|
|
@ -184,12 +171,12 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff when nested change") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(bool = true)))
|
||||
lv.el.syncAll()
|
||||
lv.el.setAllUnchanged()
|
||||
lv.handleEvent(UpdateEvent(_.copy(bool = true, nestedTitle = "nested title updated")))
|
||||
model.update(_.copy(bool = true))
|
||||
el.syncAll()
|
||||
el.setAllUnchanged()
|
||||
model.update(_.copy(bool = true, nestedTitle = "nested title updated"))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
|
|
@ -209,30 +196,27 @@ object LiveViewSpec extends TestSuite:
|
|||
NestedModel("c", 20)
|
||||
)
|
||||
)
|
||||
val lv =
|
||||
new LiveView[UpdateEvent]:
|
||||
val model = Var(initModel)
|
||||
val el =
|
||||
div(
|
||||
ul(
|
||||
model(_.items).splitByIndex((_, elem) =>
|
||||
li(
|
||||
"Nom: ",
|
||||
elem(_.name),
|
||||
" Age: ",
|
||||
elem(_.age.toString)
|
||||
)
|
||||
)
|
||||
val model = Var(initModel)
|
||||
val el =
|
||||
div(
|
||||
ul(
|
||||
model(_.items).splitByIndex((_, elem) =>
|
||||
li(
|
||||
"Nom: ",
|
||||
elem(_.name),
|
||||
" Age: ",
|
||||
elem(_.age.toString)
|
||||
)
|
||||
)
|
||||
def handleEvent = evt => model.update(evt.f)
|
||||
)
|
||||
)
|
||||
|
||||
lv.el.syncAll()
|
||||
lv.el.setAllUnchanged()
|
||||
el.syncAll()
|
||||
el.setAllUnchanged()
|
||||
|
||||
test("init") {
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json
|
||||
.Obj(
|
||||
"s" -> Json.Arr(Json.Str("<div><ul>"), Json.Str("</ul></div>")),
|
||||
|
|
@ -263,18 +247,16 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff no update") {
|
||||
assertEqualsDiff(lv.el, emptyDiff)
|
||||
assertEqualsDiff(el, emptyDiff)
|
||||
}
|
||||
test("diff with unrelated update") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(title = "title updated")))
|
||||
assertEqualsDiff(lv.el, emptyDiff)
|
||||
model.update(_.copy(title = "title updated"))
|
||||
assertEqualsDiff(el, emptyDiff)
|
||||
}
|
||||
test("diff with item changed") {
|
||||
lv.handleEvent(
|
||||
UpdateEvent(_.copy(items = initModel.items.updated(2, NestedModel("c", 99))))
|
||||
)
|
||||
model.update(_.copy(items = initModel.items.updated(2, NestedModel("c", 99))))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
|
|
@ -290,13 +272,9 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff with item added") {
|
||||
lv.handleEvent(
|
||||
UpdateEvent(
|
||||
_.copy(items = initModel.items.appended(NestedModel("d", 35)))
|
||||
)
|
||||
)
|
||||
model.update(_.copy(items = initModel.items.appended(NestedModel("d", 35))))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
|
|
@ -313,13 +291,9 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff with first item removed") {
|
||||
lv.handleEvent(
|
||||
UpdateEvent(
|
||||
_.copy(items = initModel.items.tail)
|
||||
)
|
||||
)
|
||||
model.update(_.copy(items = initModel.items.tail))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
|
|
@ -340,9 +314,9 @@ object LiveViewSpec extends TestSuite:
|
|||
)
|
||||
}
|
||||
test("diff all removed") {
|
||||
lv.handleEvent(UpdateEvent(_.copy(items = List.empty)))
|
||||
model.update(_.copy(items = List.empty))
|
||||
assertEqualsDiff(
|
||||
lv.el,
|
||||
el,
|
||||
Json.Obj(
|
||||
"0" ->
|
||||
Json
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue