diff --git a/core/src/scalive/Diff.scala b/core/src/scalive/Diff.scala new file mode 100644 index 0000000..1c8735d --- /dev/null +++ b/core/src/scalive/Diff.scala @@ -0,0 +1,51 @@ +package scalive + +import zio.Chunk +import zio.json.JsonEncoder +import zio.json.ast.Json + +enum Diff: + case Tag( + static: Seq[String] = Seq.empty, + dynamic: Seq[Diff.Dynamic] = Seq.empty + ) + case Split( + static: Seq[String] = Seq.empty, + entries: Seq[Diff.Dynamic] = Seq.empty + ) + case Static(value: String) + case Dynamic(index: Int, diff: Diff) + case Deleted + +object Diff: + given JsonEncoder[Diff] = JsonEncoder[Json].contramap(toJson) + + private def toJson(diff: Diff): Json = + diff match + case Diff.Tag(static, dynamic) => + Json.Obj( + Option + .when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*)) + .to(Chunk) + .appendedAll( + dynamic.map(d => d.index.toString -> toJson(d.diff)) + ) + ) + case Diff.Split(static, entries) => + Json.Obj( + Option + .when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*)) + .to(Chunk) + .appendedAll( + Option.when(entries.nonEmpty)( + "d" -> + Json.Obj( + entries.map(d => d.index.toString -> toJson(d.diff))* + ) + ) + ) + ) + case Diff.Static(value) => Json.Str(value) + case Diff.Dynamic(index, diff) => + Json.Obj(index.toString -> toJson(diff)) + case Diff.Deleted => Json.Bool(false) diff --git a/core/src/scalive/DiffBuilder.scala b/core/src/scalive/DiffBuilder.scala new file mode 100644 index 0000000..689dc08 --- /dev/null +++ b/core/src/scalive/DiffBuilder.scala @@ -0,0 +1,70 @@ +package scalive + +object DiffBuilder: + def build( + static: Seq[String], + dynamic: Seq[LiveMod[?]], + includeUnchanged: Boolean = false + ): Diff.Tag = + Diff.Tag( + static = static, + dynamic = dynamic.zipWithIndex + .filter(includeUnchanged || _._1.wasUpdated) + .map { + case (v: LiveMod.Dynamic[?, ?], i) => + Diff.Dynamic(i, Diff.Static(v.currentValue.toString)) + case (v: LiveMod.When[?], i) => build(v, i, includeUnchanged) + case (v: LiveMod.Split[?, ?], i) => + Diff.Dynamic(i, build(v, includeUnchanged)) + } + ) + + private def build( + mod: LiveMod.When[?], + index: Int, + includeUnchanged: Boolean + ): Diff.Dynamic = + if mod.displayed then + if includeUnchanged || mod.cond.wasUpdated then + Diff.Dynamic( + index, + build( + mod.nested.static, + mod.nested.dynamic, + includeUnchanged = true + ) + ) + else + Diff.Dynamic( + index, + build( + static = Seq.empty, + mod.nested.dynamic, + includeUnchanged + ) + ) + else Diff.Dynamic(index, Diff.Deleted) + + private def build( + mod: LiveMod.Split[?, ?], + includeUnchanged: Boolean + ): Diff.Split = + Diff.Split( + static = if includeUnchanged then mod.static else Seq.empty, + entries = mod.dynamic.toList.zipWithIndex + .filter(includeUnchanged || _._1.exists(_.wasUpdated)) + .map[Diff.Dynamic]((mods, i) => + Diff.Dynamic( + i, + build( + static = Seq.empty, + dynamic = mods, + includeUnchanged + ) + ) + ) + .appendedAll( + mod.removedIndexes + .map(i => Diff.Dynamic(i, Diff.Deleted)) + ) + ) diff --git a/core/src/scalive/DiffEngine.scala b/core/src/scalive/DiffEngine.scala deleted file mode 100644 index 3cb7e8e..0000000 --- a/core/src/scalive/DiffEngine.scala +++ /dev/null @@ -1,91 +0,0 @@ -package scalive - -import zio.json.ast.* - -enum Diff: - case Mod( - static: Seq[String] = Seq.empty, - dynamic: Seq[Diff.Dynamic] = Seq.empty - ) - case Split( - static: Seq[String] = Seq.empty, - dynamic: Seq[Diff.Dynamic] = Seq.empty - ) - case Static(value: String) - case Dynamic(index: Int, diff: Diff) - case Deleted - -object DiffEngine: - - def buildInitJson(lv: RenderedLiveView[?]): Json = - JsonAstBuilder - .diffToJson( - buildDiffValue(lv.static, lv.dynamic, includeUnchanged = true) - ) - - def buildDiffJson(lv: RenderedLiveView[?]): Json = - Json - .Obj( - "diff" -> JsonAstBuilder.diffToJson( - buildDiffValue(static = Seq.empty, lv.dynamic) - ) - ) - - private def buildDiffValue( - static: Seq[String], - dynamic: Seq[RenderedMod[?]], - includeUnchanged: Boolean = false - ): Diff.Mod = - Diff.Mod( - static = static, - dynamic = dynamic.zipWithIndex - .filter(includeUnchanged || _._1.wasUpdated) - .map { - case (v: RenderedMod.Dynamic[?, ?], i) => - Diff.Dynamic(i, Diff.Static(v.currentValue.toString)) - case (v: RenderedMod.When[?], i) => - if v.displayed then - if includeUnchanged || v.cond.wasUpdated then - Diff.Dynamic( - i, - buildDiffValue( - v.nested.static, - v.nested.dynamic, - includeUnchanged = true - ) - ) - else - Diff.Dynamic( - i, - buildDiffValue( - static = Seq.empty, - v.nested.dynamic, - includeUnchanged - ) - ) - else Diff.Dynamic(i, Diff.Deleted) - case (v: RenderedMod.Split[?, ?], i) => - Diff.Dynamic( - i, - Diff.Split( - static = if includeUnchanged then v.static else Seq.empty, - dynamic = v.dynamic.toList.zipWithIndex - .filter(includeUnchanged || _._1.exists(_.wasUpdated)) - .map[Diff.Dynamic]((mods, i) => - Diff.Dynamic( - i, - buildDiffValue( - static = Seq.empty, - dynamic = mods, - includeUnchanged - ) - ) - ) - .appendedAll( - v.removedIndexes - .map(i => Diff.Dynamic(i, Diff.Deleted)) - ) - ) - ) - } - ) diff --git a/core/src/scalive/JsonAstBuilder.scala b/core/src/scalive/JsonAstBuilder.scala deleted file mode 100644 index 4cd9173..0000000 --- a/core/src/scalive/JsonAstBuilder.scala +++ /dev/null @@ -1,36 +0,0 @@ -package scalive - -import zio.Chunk -import zio.json.ast.* - -object JsonAstBuilder: - - def diffToJson(diff: Diff): Json = - diff match - case Diff.Mod(static, dynamic) => - Json.Obj( - Option - .when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*)) - .to(Chunk) - .appendedAll( - dynamic.map(d => d.index.toString -> diffToJson(d.diff)) - ) - ) - case Diff.Split(static, dynamic) => - Json.Obj( - Option - .when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*)) - .to(Chunk) - .appendedAll( - Option.when(dynamic.nonEmpty)( - "d" -> - Json.Obj( - dynamic.map(d => d.index.toString -> diffToJson(d.diff))* - ) - ) - ) - ) - case Diff.Static(value) => Json.Str(value) - case Diff.Dynamic(index, diff) => - Json.Obj(index.toString -> diffToJson(diff)) - case Diff.Deleted => Json.Bool(false) diff --git a/core/src/scalive/LiveMod.scala b/core/src/scalive/LiveMod.scala new file mode 100644 index 0000000..2fdafa2 --- /dev/null +++ b/core/src/scalive/LiveMod.scala @@ -0,0 +1,62 @@ +package scalive + +import scala.collection.immutable.ArraySeq +import scala.collection.mutable.ArrayBuffer + +sealed trait LiveMod[Model]: + def update(model: Model): Unit + def wasUpdated: Boolean + +object LiveMod: + + class Dynamic[I, O](d: Dyn[I, O], init: I, startsUpdated: Boolean = false) + extends LiveMod[I]: + private var value: O = d.run(init) + private var updated: Boolean = startsUpdated + def wasUpdated: Boolean = updated + def currentValue: O = value + def update(v: I): Unit = + val newValue = d.run(v) + if value == newValue then updated = false + else + value = newValue + updated = true + + class When[Model]( + dynCond: Dyn[Model, Boolean], + tag: HtmlTag[Model], + init: Model + ) extends LiveMod[Model]: + val cond = LiveMod.Dynamic(dynCond, init) + val nested = LiveView.render(tag, init) + def displayed: Boolean = cond.currentValue + def wasUpdated: Boolean = cond.wasUpdated || nested.wasUpdated + def update(model: Model): Unit = + cond.update(model) + nested.update(model) + + class Split[Model, Item]( + dynList: Dyn[Model, List[Item]], + project: Dyn[Item, Item] => HtmlTag[Item], + init: Model + ) extends LiveMod[Model]: + private val tag = project(Dyn.id) + val static: ArraySeq[String] = LiveView.buildStatic(tag) + val dynamic: ArrayBuffer[ArraySeq[LiveMod[Item]]] = + dynList.run(init).map(LiveView.buildDynamic(tag, _)).to(ArrayBuffer) + var removedIndexes: Seq[Int] = Seq.empty + + def wasUpdated: Boolean = + removedIndexes.nonEmpty || dynamic.exists(_.exists(_.wasUpdated)) + + def update(model: Model): Unit = + val items = dynList.run(model) + removedIndexes = + if items.size < dynamic.size then items.size until dynamic.size + else Seq.empty + dynamic.takeInPlace(items.size) + items.zipWithIndex.map((item, i) => + if i >= dynamic.size then + dynamic.append(LiveView.buildDynamic(tag, item, startsUpdated = true)) + else dynamic(i).foreach(_.update(item)) + ) diff --git a/core/src/scalive/LiveView.scala b/core/src/scalive/LiveView.scala index 37fee7c..259c777 100644 --- a/core/src/scalive/LiveView.scala +++ b/core/src/scalive/LiveView.scala @@ -1,50 +1,85 @@ package scalive -trait LiveView[Model]: - val model: Dyn[Model, Model] = Dyn.id - def view: HtmlTag[Model] +import scala.annotation.nowarn +import scala.collection.immutable.ArraySeq +import scala.collection.mutable.ListBuffer -opaque type Dyn[I, O] = I => O -extension [I, O](d: Dyn[I, O]) - def apply[O2](f: O => O2): Dyn[I, O2] = d.andThen(f) +class LiveView[Model] private ( + val static: ArraySeq[String], + val dynamic: ArraySeq[LiveMod[Model]] +): + def update(model: Model): Unit = + dynamic.foreach(_.update(model)) - def when(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] = - Mod.When(d.andThen(f), tag) + def wasUpdated: Boolean = dynamic.exists(_.wasUpdated) - inline def whenNot(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] = - when(f.andThen(!_))(tag) + def fullDiff: Diff = + DiffBuilder.build(static, dynamic, includeUnchanged = true) - def splitByIndex[O2](f: O => List[O2])( - project: Dyn[O2, O2] => HtmlTag[O2] - ): Mod.Split[I, O2] = - Mod.Split(d.andThen(f), project) + def diff: Diff = + DiffBuilder.build(static = Seq.empty, dynamic) - def run(v: I): O = d(v) +object LiveView: -object Dyn: - def id[T]: Dyn[T, T] = identity + def apply[Model]( + lv: View[Model], + model: Model + ): LiveView[Model] = + render(lv.view, model) -enum Mod[T]: - case Tag(tag: HtmlTag[T]) - case Text(text: String) - case DynText(dynText: Dyn[T, String]) - case When(dynCond: Dyn[T, Boolean], tag: HtmlTag[T]) - case Split[T, O]( - dynList: Dyn[T, List[O]], - project: Dyn[O, O] => HtmlTag[O] - ) extends Mod[T] + def buildStatic[Model](tag: HtmlTag[Model]): ArraySeq[String] = + buildNestedStatic(tag).flatten.to(ArraySeq) -given [T]: Conversion[HtmlTag[T], Mod[T]] = Mod.Tag(_) -given [T]: Conversion[String, Mod[T]] = Mod.Text(_) -given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_) + private def buildNestedStatic[Model]( + tag: HtmlTag[Model] + ): Seq[Option[String]] = + val static = ListBuffer.empty[Option[String]] + var staticFragment = s"<${tag.name}>" + for mod <- tag.mods.flatMap(buildStatic) do + mod match + case Some(s) => + staticFragment += s + case None => + static.append(Some(staticFragment)) + static.append(None) + staticFragment = "" + staticFragment += s"${tag.name}>" + static.append(Some(staticFragment)) + static.toSeq -trait HtmlTag[Model](val name: String): - def mods: List[Mod[Model]] + def buildStatic[Model](mod: Mod[Model]): Seq[Option[String]] = + mod match + case Mod.Tag(tag) => buildNestedStatic(tag) + case Mod.Text(text) => List(Some(text)) + case Mod.DynText(_) => List(None) + case Mod.When(_, _) => List(None) + case Mod.Split(_, _) => List(None) -class Div[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("div") -class Ul[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("ul") -class Li[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("li") + def buildDynamic[Model]( + tag: HtmlTag[Model], + model: Model, + startsUpdated: Boolean = false + ): ArraySeq[LiveMod[Model]] = + tag.mods.flatMap(buildDynamic(_, model, startsUpdated)).to(ArraySeq) -def div[Model](mods: Mod[Model]*): Div[Model] = Div(mods.toList) -def ul[Model](mods: Mod[Model]*): Ul[Model] = Ul(mods.toList) -def li[Model](mods: Mod[Model]*): Li[Model] = Li(mods.toList) + @nowarn("cat=unchecked") + def buildDynamic[Model]( + mod: Mod[Model], + model: Model, + startsUpdated: Boolean + ): Seq[LiveMod[Model]] = + mod match + case Mod.Tag(tag) => buildDynamic(tag, model, startsUpdated) + case Mod.Text(text) => List.empty + case Mod.DynText[Model](dynText) => + List(LiveMod.Dynamic(dynText, model, startsUpdated)) + case Mod.When[Model](dynCond, tag) => + List(LiveMod.When(dynCond, tag, model)) + case Mod.Split[Model, Any](dynList, project) => + List(LiveMod.Split(dynList, project, model)) + + def render[Model]( + tag: HtmlTag[Model], + model: Model + ): LiveView[Model] = + new LiveView(buildStatic(tag), buildDynamic(tag, model)) diff --git a/core/src/scalive/LiveViewRenderer.scala b/core/src/scalive/LiveViewRenderer.scala deleted file mode 100644 index 5071352..0000000 --- a/core/src/scalive/LiveViewRenderer.scala +++ /dev/null @@ -1,140 +0,0 @@ -package scalive - -import scalive.LiveViewRenderer.buildDynamic -import scalive.LiveViewRenderer.buildStatic - -import scala.annotation.nowarn -import scala.collection.immutable.ArraySeq -import scala.collection.mutable.ArrayBuffer -import scala.collection.mutable.ListBuffer - -class RenderedLiveView[Model] private[scalive] ( - val static: ArraySeq[String], - val dynamic: ArraySeq[RenderedMod[Model]] -): - def update(model: Model): Unit = - dynamic.foreach(_.update(model)) - def wasUpdated: Boolean = dynamic.exists(_.wasUpdated) - -sealed trait RenderedMod[Model]: - def update(model: Model): Unit - def wasUpdated: Boolean - -object RenderedMod: - - class Dynamic[I, O](d: Dyn[I, O], init: I, startsUpdated: Boolean = false) - extends RenderedMod[I]: - private var value: O = d.run(init) - private var updated: Boolean = startsUpdated - def wasUpdated: Boolean = updated - def currentValue: O = value - def update(v: I): Unit = - val newValue = d.run(v) - if value == newValue then updated = false - else - value = newValue - updated = true - - class When[Model]( - dynCond: Dyn[Model, Boolean], - tag: HtmlTag[Model], - init: Model - ) extends RenderedMod[Model]: - val cond = RenderedMod.Dynamic(dynCond, init) - val nested = LiveViewRenderer.render(tag, init) - def displayed: Boolean = cond.currentValue - def wasUpdated: Boolean = cond.wasUpdated || nested.wasUpdated - def update(model: Model): Unit = - cond.update(model) - nested.update(model) - - class Split[Model, Item]( - dynList: Dyn[Model, List[Item]], - project: Dyn[Item, Item] => HtmlTag[Item], - init: Model - ) extends RenderedMod[Model]: - private val tag = project(Dyn.id) - val static: ArraySeq[String] = buildStatic(tag) - val dynamic: ArrayBuffer[ArraySeq[RenderedMod[Item]]] = - dynList.run(init).map(buildDynamic(tag, _)).to(ArrayBuffer) - var removedIndexes: Seq[Int] = Seq.empty - - def wasUpdated: Boolean = - removedIndexes.nonEmpty || dynamic.exists(_.exists(_.wasUpdated)) - - def update(model: Model): Unit = - val items = dynList.run(model) - removedIndexes = - if items.size < dynamic.size then items.size until dynamic.size - else Seq.empty - dynamic.takeInPlace(items.size) - items.zipWithIndex.map((item, i) => - if i >= dynamic.size then - dynamic.append(buildDynamic(tag, item, startsUpdated = true)) - else dynamic(i).foreach(_.update(item)) - ) - -object LiveViewRenderer: - - def render[Model]( - lv: LiveView[Model], - model: Model - ): RenderedLiveView[Model] = - render(lv.view, model) - - def buildStatic[Model](tag: HtmlTag[Model]): ArraySeq[String] = - buildNestedStatic(tag).flatten.to(ArraySeq) - - private def buildNestedStatic[Model]( - tag: HtmlTag[Model] - ): Seq[Option[String]] = - val static = ListBuffer.empty[Option[String]] - var staticFragment = s"<${tag.name}>" - for mod <- tag.mods.flatMap(buildStatic) do - mod match - case Some(s) => - staticFragment += s - case None => - static.append(Some(staticFragment)) - static.append(None) - staticFragment = "" - staticFragment += s"${tag.name}>" - static.append(Some(staticFragment)) - static.toSeq - - def buildStatic[Model](mod: Mod[Model]): Seq[Option[String]] = - mod match - case Mod.Tag(tag) => buildNestedStatic(tag) - case Mod.Text(text) => List(Some(text)) - case Mod.DynText(_) => List(None) - case Mod.When(_, _) => List(None) - case Mod.Split(_, _) => List(None) - - def buildDynamic[Model]( - tag: HtmlTag[Model], - model: Model, - startsUpdated: Boolean = false - ): ArraySeq[RenderedMod[Model]] = - tag.mods.flatMap(buildDynamic(_, model, startsUpdated)).to(ArraySeq) - - @nowarn("cat=unchecked") - def buildDynamic[Model]( - mod: Mod[Model], - model: Model, - startsUpdated: Boolean - ): Seq[RenderedMod[Model]] = - mod match - case Mod.Tag(tag) => buildDynamic(tag, model, startsUpdated) - case Mod.Text(text) => List.empty - case Mod.DynText[Model](dynText) => - List(RenderedMod.Dynamic(dynText, model, startsUpdated)) - case Mod.When[Model](dynCond, tag) => - List(RenderedMod.When(dynCond, tag, model)) - case Mod.Split[Model, Any](dynList, project) => - List(RenderedMod.Split(dynList, project, model)) - - def render[Model]( - tag: HtmlTag[Model], - model: Model - ): RenderedLiveView[Model] = - new RenderedLiveView(buildStatic(tag), buildDynamic(tag, model)) diff --git a/core/src/scalive/View.scala b/core/src/scalive/View.scala new file mode 100644 index 0000000..7a58181 --- /dev/null +++ b/core/src/scalive/View.scala @@ -0,0 +1,50 @@ +package scalive + +trait View[Model]: + val model: Dyn[Model, Model] = Dyn.id + def view: HtmlTag[Model] + +opaque type Dyn[I, O] = I => O +extension [I, O](d: Dyn[I, O]) + def apply[O2](f: O => O2): Dyn[I, O2] = d.andThen(f) + + def when(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] = + Mod.When(d.andThen(f), tag) + + inline def whenNot(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] = + when(f.andThen(!_))(tag) + + def splitByIndex[O2](f: O => List[O2])( + project: Dyn[O2, O2] => HtmlTag[O2] + ): Mod.Split[I, O2] = + Mod.Split(d.andThen(f), project) + + def run(v: I): O = d(v) + +object Dyn: + def id[T]: Dyn[T, T] = identity + +enum Mod[T]: + case Tag(tag: HtmlTag[T]) + case Text(text: String) + case DynText(dynText: Dyn[T, String]) + case When(dynCond: Dyn[T, Boolean], tag: HtmlTag[T]) + case Split[T, O]( + dynList: Dyn[T, List[O]], + project: Dyn[O, O] => HtmlTag[O] + ) extends Mod[T] + +given [T]: Conversion[HtmlTag[T], Mod[T]] = Mod.Tag(_) +given [T]: Conversion[String, Mod[T]] = Mod.Text(_) +given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_) + +trait HtmlTag[Model](val name: String): + def mods: List[Mod[Model]] + +class Div[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("div") +class Ul[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("ul") +class Li[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("li") + +def div[Model](mods: Mod[Model]*): Div[Model] = Div(mods.toList) +def ul[Model](mods: Mod[Model]*): Ul[Model] = Ul(mods.toList) +def li[Model](mods: Mod[Model]*): Li[Model] = Li(mods.toList) diff --git a/core/src/scalive/main.scala b/core/src/scalive/main.scala index dda0b9f..d6486b9 100644 --- a/core/src/scalive/main.scala +++ b/core/src/scalive/main.scala @@ -4,9 +4,9 @@ import zio.json.* @main def main = - val r = - LiveViewRenderer.render( - TestLiveView, + val lv = + LiveView( + TestView, MyModel( List( NestedModel("a", 10), @@ -15,9 +15,10 @@ def main = ) ) ) - println(DiffEngine.buildInitJson(r).toJsonPretty) + println(lv.fullDiff.toJsonPretty) + println("Edit first and last") - r.update( + lv.update( MyModel( List( NestedModel("x", 10), @@ -26,9 +27,10 @@ def main = ) ) ) - println(DiffEngine.buildDiffJson(r).toJsonPretty) + println(lv.diff.toJsonPretty) + println("Add one") - r.update( + lv.update( MyModel( List( NestedModel("x", 10), @@ -38,9 +40,10 @@ def main = ) ) ) - println(DiffEngine.buildDiffJson(r).toJsonPretty) + println(lv.diff.toJsonPretty) + println("Remove first") - r.update( + lv.update( MyModel( List( NestedModel("b", 15), @@ -49,17 +52,18 @@ def main = ) ) ) - println(DiffEngine.buildDiffJson(r).toJsonPretty) + println(lv.diff.toJsonPretty) + println("Remove all") - r.update( + lv.update( MyModel(List.empty) ) - println(DiffEngine.buildDiffJson(r).toJsonPretty) + println(lv.diff.toJsonPretty) final case class MyModel(elems: List[NestedModel]) final case class NestedModel(name: String, age: Int) -object TestLiveView extends LiveView[MyModel]: +object TestView extends View[MyModel]: val view: HtmlTag[MyModel] = div( ul( diff --git a/core/test/src/scalive/LiveViewSpec.scala b/core/test/src/scalive/LiveViewSpec.scala index a85afad..bcae4ba 100644 --- a/core/test/src/scalive/LiveViewSpec.scala +++ b/core/test/src/scalive/LiveViewSpec.scala @@ -14,17 +14,17 @@ object LiveViewSpec extends TestSuite: ) final case class NestedModel(name: String, age: Int) - def assertEqualsJson(actual: Json, expected: Json) = + def assertEqualsJson(actual: Diff, expected: Json) = assert(actual.toJsonPretty == expected.toJsonPretty) - val emptyDiff = Json.Obj("diff" -> Json.Obj.empty) + val emptyDiff = Json.Obj.empty val tests = Tests { test("Static only") { val lv = - LiveViewRenderer.render( - new LiveView[Unit]: + LiveView( + new View[Unit]: val view: HtmlTag[Unit] = div("Static string") , @@ -32,21 +32,21 @@ object LiveViewSpec extends TestSuite: ) test("init") { assertEqualsJson( - DiffEngine.buildInitJson(lv), + lv.fullDiff, Json.Obj( "s" -> Json.Arr(Json.Str("