mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Reorganize
This commit is contained in:
parent
844a1f7953
commit
c523ba7858
10 changed files with 401 additions and 409 deletions
51
core/src/scalive/Diff.scala
Normal file
51
core/src/scalive/Diff.scala
Normal file
|
|
@ -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)
|
||||||
70
core/src/scalive/DiffBuilder.scala
Normal file
70
core/src/scalive/DiffBuilder.scala
Normal file
|
|
@ -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))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
@ -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))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
@ -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)
|
|
||||||
62
core/src/scalive/LiveMod.scala
Normal file
62
core/src/scalive/LiveMod.scala
Normal file
|
|
@ -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))
|
||||||
|
)
|
||||||
|
|
@ -1,50 +1,85 @@
|
||||||
package scalive
|
package scalive
|
||||||
|
|
||||||
trait LiveView[Model]:
|
import scala.annotation.nowarn
|
||||||
val model: Dyn[Model, Model] = Dyn.id
|
import scala.collection.immutable.ArraySeq
|
||||||
def view: HtmlTag[Model]
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
opaque type Dyn[I, O] = I => O
|
class LiveView[Model] private (
|
||||||
extension [I, O](d: Dyn[I, O])
|
val static: ArraySeq[String],
|
||||||
def apply[O2](f: O => O2): Dyn[I, O2] = d.andThen(f)
|
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] =
|
def wasUpdated: Boolean = dynamic.exists(_.wasUpdated)
|
||||||
Mod.When(d.andThen(f), tag)
|
|
||||||
|
|
||||||
inline def whenNot(f: O => Boolean)(tag: HtmlTag[I]): Mod.When[I] =
|
def fullDiff: Diff =
|
||||||
when(f.andThen(!_))(tag)
|
DiffBuilder.build(static, dynamic, includeUnchanged = true)
|
||||||
|
|
||||||
def splitByIndex[O2](f: O => List[O2])(
|
def diff: Diff =
|
||||||
project: Dyn[O2, O2] => HtmlTag[O2]
|
DiffBuilder.build(static = Seq.empty, dynamic)
|
||||||
): Mod.Split[I, O2] =
|
|
||||||
Mod.Split(d.andThen(f), project)
|
|
||||||
|
|
||||||
def run(v: I): O = d(v)
|
object LiveView:
|
||||||
|
|
||||||
object Dyn:
|
def apply[Model](
|
||||||
def id[T]: Dyn[T, T] = identity
|
lv: View[Model],
|
||||||
|
model: Model
|
||||||
|
): LiveView[Model] =
|
||||||
|
render(lv.view, model)
|
||||||
|
|
||||||
enum Mod[T]:
|
def buildStatic[Model](tag: HtmlTag[Model]): ArraySeq[String] =
|
||||||
case Tag(tag: HtmlTag[T])
|
buildNestedStatic(tag).flatten.to(ArraySeq)
|
||||||
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(_)
|
private def buildNestedStatic[Model](
|
||||||
given [T]: Conversion[String, Mod[T]] = Mod.Text(_)
|
tag: HtmlTag[Model]
|
||||||
given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_)
|
): 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 buildStatic[Model](mod: Mod[Model]): Seq[Option[String]] =
|
||||||
def mods: List[Mod[Model]]
|
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")
|
def buildDynamic[Model](
|
||||||
class Ul[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("ul")
|
tag: HtmlTag[Model],
|
||||||
class Li[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]("li")
|
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)
|
@nowarn("cat=unchecked")
|
||||||
def ul[Model](mods: Mod[Model]*): Ul[Model] = Ul(mods.toList)
|
def buildDynamic[Model](
|
||||||
def li[Model](mods: Mod[Model]*): Li[Model] = Li(mods.toList)
|
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))
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
50
core/src/scalive/View.scala
Normal file
50
core/src/scalive/View.scala
Normal file
|
|
@ -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)
|
||||||
|
|
@ -4,9 +4,9 @@ import zio.json.*
|
||||||
|
|
||||||
@main
|
@main
|
||||||
def main =
|
def main =
|
||||||
val r =
|
val lv =
|
||||||
LiveViewRenderer.render(
|
LiveView(
|
||||||
TestLiveView,
|
TestView,
|
||||||
MyModel(
|
MyModel(
|
||||||
List(
|
List(
|
||||||
NestedModel("a", 10),
|
NestedModel("a", 10),
|
||||||
|
|
@ -15,9 +15,10 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
println(DiffEngine.buildInitJson(r).toJsonPretty)
|
println(lv.fullDiff.toJsonPretty)
|
||||||
|
|
||||||
println("Edit first and last")
|
println("Edit first and last")
|
||||||
r.update(
|
lv.update(
|
||||||
MyModel(
|
MyModel(
|
||||||
List(
|
List(
|
||||||
NestedModel("x", 10),
|
NestedModel("x", 10),
|
||||||
|
|
@ -26,9 +27,10 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
println(DiffEngine.buildDiffJson(r).toJsonPretty)
|
println(lv.diff.toJsonPretty)
|
||||||
|
|
||||||
println("Add one")
|
println("Add one")
|
||||||
r.update(
|
lv.update(
|
||||||
MyModel(
|
MyModel(
|
||||||
List(
|
List(
|
||||||
NestedModel("x", 10),
|
NestedModel("x", 10),
|
||||||
|
|
@ -38,9 +40,10 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
println(DiffEngine.buildDiffJson(r).toJsonPretty)
|
println(lv.diff.toJsonPretty)
|
||||||
|
|
||||||
println("Remove first")
|
println("Remove first")
|
||||||
r.update(
|
lv.update(
|
||||||
MyModel(
|
MyModel(
|
||||||
List(
|
List(
|
||||||
NestedModel("b", 15),
|
NestedModel("b", 15),
|
||||||
|
|
@ -49,17 +52,18 @@ def main =
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
println(DiffEngine.buildDiffJson(r).toJsonPretty)
|
println(lv.diff.toJsonPretty)
|
||||||
|
|
||||||
println("Remove all")
|
println("Remove all")
|
||||||
r.update(
|
lv.update(
|
||||||
MyModel(List.empty)
|
MyModel(List.empty)
|
||||||
)
|
)
|
||||||
println(DiffEngine.buildDiffJson(r).toJsonPretty)
|
println(lv.diff.toJsonPretty)
|
||||||
|
|
||||||
final case class MyModel(elems: List[NestedModel])
|
final case class MyModel(elems: List[NestedModel])
|
||||||
final case class NestedModel(name: String, age: Int)
|
final case class NestedModel(name: String, age: Int)
|
||||||
|
|
||||||
object TestLiveView extends LiveView[MyModel]:
|
object TestView extends View[MyModel]:
|
||||||
val view: HtmlTag[MyModel] =
|
val view: HtmlTag[MyModel] =
|
||||||
div(
|
div(
|
||||||
ul(
|
ul(
|
||||||
|
|
|
||||||
|
|
@ -14,17 +14,17 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
final case class NestedModel(name: String, age: Int)
|
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)
|
assert(actual.toJsonPretty == expected.toJsonPretty)
|
||||||
|
|
||||||
val emptyDiff = Json.Obj("diff" -> Json.Obj.empty)
|
val emptyDiff = Json.Obj.empty
|
||||||
|
|
||||||
val tests = Tests {
|
val tests = Tests {
|
||||||
|
|
||||||
test("Static only") {
|
test("Static only") {
|
||||||
val lv =
|
val lv =
|
||||||
LiveViewRenderer.render(
|
LiveView(
|
||||||
new LiveView[Unit]:
|
new View[Unit]:
|
||||||
val view: HtmlTag[Unit] =
|
val view: HtmlTag[Unit] =
|
||||||
div("Static string")
|
div("Static string")
|
||||||
,
|
,
|
||||||
|
|
@ -32,21 +32,21 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
test("init") {
|
test("init") {
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildInitJson(lv),
|
lv.fullDiff,
|
||||||
Json.Obj(
|
Json.Obj(
|
||||||
"s" -> Json.Arr(Json.Str("<div>Static string</div>"))
|
"s" -> Json.Arr(Json.Str("<div>Static string</div>"))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
test("diff") {
|
test("diff") {
|
||||||
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
|
assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Dynamic string") {
|
test("Dynamic string") {
|
||||||
val lv =
|
val lv =
|
||||||
LiveViewRenderer.render(
|
LiveView(
|
||||||
new LiveView[TestModel]:
|
new View[TestModel]:
|
||||||
val view: HtmlTag[TestModel] =
|
val view: HtmlTag[TestModel] =
|
||||||
div(model(_.title))
|
div(model(_.title))
|
||||||
,
|
,
|
||||||
|
|
@ -54,7 +54,7 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
test("init") {
|
test("init") {
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildInitJson(lv),
|
lv.fullDiff,
|
||||||
Json
|
Json
|
||||||
.Obj(
|
.Obj(
|
||||||
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
||||||
|
|
@ -63,28 +63,26 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
test("diff no update") {
|
test("diff no update") {
|
||||||
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
|
assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
}
|
||||||
test("diff with update") {
|
test("diff with update") {
|
||||||
lv.update(TestModel(title = "title updated"))
|
lv.update(TestModel(title = "title updated"))
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildDiffJson(lv),
|
lv.diff,
|
||||||
Json.Obj(
|
Json.Obj("0" -> Json.Str("title updated"))
|
||||||
"diff" -> Json.Obj("0" -> Json.Str("title updated"))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
test("diff with update and no change") {
|
test("diff with update and no change") {
|
||||||
lv.update(TestModel(title = "title updated"))
|
lv.update(TestModel(title = "title updated"))
|
||||||
lv.update(TestModel(title = "title updated"))
|
lv.update(TestModel(title = "title updated"))
|
||||||
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
|
assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("when mod") {
|
test("when mod") {
|
||||||
val lv =
|
val lv =
|
||||||
LiveViewRenderer.render(
|
LiveView(
|
||||||
new LiveView[TestModel]:
|
new View[TestModel]:
|
||||||
val view: HtmlTag[TestModel] =
|
val view: HtmlTag[TestModel] =
|
||||||
div(
|
div(
|
||||||
model.when(_.bool)(
|
model.when(_.bool)(
|
||||||
|
|
@ -96,7 +94,7 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
test("init") {
|
test("init") {
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildInitJson(lv),
|
lv.fullDiff,
|
||||||
Json
|
Json
|
||||||
.Obj(
|
.Obj(
|
||||||
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
|
||||||
|
|
@ -105,26 +103,24 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
test("diff no update") {
|
test("diff no update") {
|
||||||
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
|
assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
}
|
||||||
test("diff with unrelated update") {
|
test("diff with unrelated update") {
|
||||||
lv.update(TestModel(title = "title updated"))
|
lv.update(TestModel(title = "title updated"))
|
||||||
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
|
assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
}
|
||||||
test("diff when true") {
|
test("diff when true") {
|
||||||
lv.update(TestModel(bool = true))
|
lv.update(TestModel(bool = true))
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildDiffJson(lv),
|
lv.diff,
|
||||||
Json.Obj(
|
Json.Obj(
|
||||||
"diff" -> Json.Obj(
|
"0" ->
|
||||||
"0" ->
|
Json
|
||||||
Json
|
.Obj(
|
||||||
.Obj(
|
"s" -> Json
|
||||||
"s" -> Json
|
.Arr(Json.Str("<div>static string"), Json.Str("</div>")),
|
||||||
.Arr(Json.Str("<div>static string"), Json.Str("</div>")),
|
"0" -> Json.Str("nested title value")
|
||||||
"0" -> Json.Str("nested title value")
|
)
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -132,15 +128,13 @@ object LiveViewSpec extends TestSuite:
|
||||||
lv.update(TestModel(bool = true))
|
lv.update(TestModel(bool = true))
|
||||||
lv.update(TestModel(bool = true, nestedTitle = "nested title updated"))
|
lv.update(TestModel(bool = true, nestedTitle = "nested title updated"))
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildDiffJson(lv),
|
lv.diff,
|
||||||
Json.Obj(
|
Json.Obj(
|
||||||
"diff" -> Json.Obj(
|
"0" ->
|
||||||
"0" ->
|
Json
|
||||||
Json
|
.Obj(
|
||||||
.Obj(
|
"0" -> Json.Str("nested title updated")
|
||||||
"0" -> Json.Str("nested title updated")
|
)
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -156,8 +150,8 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val lv =
|
val lv =
|
||||||
LiveViewRenderer.render(
|
LiveView(
|
||||||
new LiveView[TestModel]:
|
new View[TestModel]:
|
||||||
val view: HtmlTag[TestModel] =
|
val view: HtmlTag[TestModel] =
|
||||||
div(
|
div(
|
||||||
ul(
|
ul(
|
||||||
|
|
@ -176,7 +170,7 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
test("init") {
|
test("init") {
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildInitJson(lv),
|
lv.fullDiff,
|
||||||
Json
|
Json
|
||||||
.Obj(
|
.Obj(
|
||||||
"s" -> Json.Arr(Json.Str("<div><ul>"), Json.Str("</ul></div>")),
|
"s" -> Json.Arr(Json.Str("<div><ul>"), Json.Str("</ul></div>")),
|
||||||
|
|
@ -205,11 +199,11 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
test("diff no update") {
|
test("diff no update") {
|
||||||
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
|
assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
}
|
||||||
test("diff with unrelated update") {
|
test("diff with unrelated update") {
|
||||||
lv.update(initModel.copy(title = "title updated"))
|
lv.update(initModel.copy(title = "title updated"))
|
||||||
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
|
assertEqualsJson(lv.diff, emptyDiff)
|
||||||
}
|
}
|
||||||
test("diff with item changed") {
|
test("diff with item changed") {
|
||||||
lv.update(
|
lv.update(
|
||||||
|
|
@ -218,19 +212,17 @@ object LiveViewSpec extends TestSuite:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildDiffJson(lv),
|
lv.diff,
|
||||||
Json.Obj(
|
Json.Obj(
|
||||||
"diff" -> Json.Obj(
|
"0" ->
|
||||||
"0" ->
|
Json
|
||||||
Json
|
.Obj(
|
||||||
.Obj(
|
"d" -> Json.Obj(
|
||||||
"d" -> Json.Obj(
|
"2" -> Json.Obj(
|
||||||
"2" -> Json.Obj(
|
"1" -> Json.Str("99")
|
||||||
"1" -> Json.Str("99")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -239,20 +231,18 @@ object LiveViewSpec extends TestSuite:
|
||||||
initModel.copy(items = initModel.items.appended(NestedModel("d", 35)))
|
initModel.copy(items = initModel.items.appended(NestedModel("d", 35)))
|
||||||
)
|
)
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildDiffJson(lv),
|
lv.diff,
|
||||||
Json.Obj(
|
Json.Obj(
|
||||||
"diff" -> Json.Obj(
|
"0" ->
|
||||||
"0" ->
|
Json
|
||||||
Json
|
.Obj(
|
||||||
.Obj(
|
"d" -> Json.Obj(
|
||||||
"d" -> Json.Obj(
|
"3" -> Json.Obj(
|
||||||
"3" -> Json.Obj(
|
"0" -> Json.Str("d"),
|
||||||
"0" -> Json.Str("d"),
|
"1" -> Json.Str("35")
|
||||||
"1" -> Json.Str("35")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -261,46 +251,43 @@ object LiveViewSpec extends TestSuite:
|
||||||
initModel.copy(items = initModel.items.tail)
|
initModel.copy(items = initModel.items.tail)
|
||||||
)
|
)
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildDiffJson(lv),
|
lv.diff,
|
||||||
Json.Obj(
|
Json.Obj(
|
||||||
"diff" -> Json.Obj(
|
"0" ->
|
||||||
"0" ->
|
Json
|
||||||
Json
|
.Obj(
|
||||||
.Obj(
|
"d" -> Json.Obj(
|
||||||
"d" -> Json.Obj(
|
"0" -> Json.Obj(
|
||||||
"0" -> Json.Obj(
|
"0" -> Json.Str("b"),
|
||||||
"0" -> Json.Str("b"),
|
"1" -> Json.Str("15")
|
||||||
"1" -> Json.Str("15")
|
),
|
||||||
),
|
"1" -> Json.Obj(
|
||||||
"1" -> Json.Obj(
|
"0" -> Json.Str("c"),
|
||||||
"0" -> Json.Str("c"),
|
"1" -> Json.Str("20")
|
||||||
"1" -> Json.Str("20")
|
),
|
||||||
),
|
"2" -> Json.Bool(false)
|
||||||
"2" -> Json.Bool(false)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
test("diff all removed") {
|
test("diff all removed") {
|
||||||
lv.update(initModel.copy(items = List.empty))
|
lv.update(initModel.copy(items = List.empty))
|
||||||
assertEqualsJson(
|
assertEqualsJson(
|
||||||
DiffEngine.buildDiffJson(lv),
|
lv.diff,
|
||||||
Json.Obj(
|
Json.Obj(
|
||||||
"diff" -> Json.Obj(
|
"0" ->
|
||||||
"0" ->
|
Json
|
||||||
Json
|
.Obj(
|
||||||
.Obj(
|
"d" -> Json.Obj(
|
||||||
"d" -> Json.Obj(
|
"0" -> Json.Bool(false),
|
||||||
"0" -> Json.Bool(false),
|
"1" -> Json.Bool(false),
|
||||||
"1" -> Json.Bool(false),
|
"2" -> Json.Bool(false)
|
||||||
"2" -> Json.Bool(false)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue