diff --git a/core/src/scalive/Diff.scala b/core/src/scalive/Diff.scala index 95ee3fd..5a81afa 100644 --- a/core/src/scalive/Diff.scala +++ b/core/src/scalive/Diff.scala @@ -10,10 +10,10 @@ enum Diff: dynamic: Seq[Diff.Dynamic] = Seq.empty) case Comprehension( static: Seq[String] = Seq.empty, - entries: Seq[Diff.Dynamic] = Seq.empty, + entries: Seq[Diff.Dynamic | Diff.IndexChange] = Seq.empty, count: Int = 0) case Value(value: String) - case Dynamic(key: String, diff: Diff) + case Dynamic(index: Int, diff: Diff) case Deleted extension (diff: Diff) @@ -27,6 +27,8 @@ extension (diff: Diff) object Diff: given JsonEncoder[Diff] = JsonEncoder[Json].contramap(toJson(_)) + final case class IndexChange(index: Int, previousIndex: Int) + private def toJson(diff: Diff): Json = diff match case Diff.Tag(static, dynamic) => @@ -35,7 +37,7 @@ object Diff: .when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*)) .to(Chunk) .appendedAll( - dynamic.map(d => d.key -> toJson(d.diff)) + dynamic.map(d => d.index.toString -> toJson(d.diff)) ) ) case Diff.Comprehension(static, entries, count) => @@ -47,7 +49,12 @@ object Diff: "k" -> Json .Obj( - entries.map(d => d.key -> toJson(d.diff))* + entries.map { + case Diff.Dynamic(index, diff) => + index.toString -> toJson(diff) + case Diff.IndexChange(index, previousIndex) => + index.toString -> Json.Num(previousIndex) + }* ).add("kc", Json.Num(count)) ) ) diff --git a/core/src/scalive/DiffBuilder.scala b/core/src/scalive/DiffBuilder.scala index c40f691..39636c7 100644 --- a/core/src/scalive/DiffBuilder.scala +++ b/core/src/scalive/DiffBuilder.scala @@ -17,7 +17,7 @@ object DiffBuilder: static = static, dynamic = buildDynamic(dynamicMods, trackUpdates).zipWithIndex.collect { case (Some(diff), index) => - Diff.Dynamic(index.toString, diff) + Diff.Dynamic(index, diff) } ) @@ -47,15 +47,24 @@ object DiffBuilder: case Some((entries, keysCount, includeStatics)) => val static = if !trackUpdates || includeStatics then - entries.collectFirst { case (_, el) => el.static }.getOrElse(List.empty) + entries.collectFirst { case (_, _, el) => el.static }.getOrElse(List.empty) else List.empty List( Some( Diff.Comprehension( static = static, - entries = entries.map((key, el) => - Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates)) - ), + entries = entries.map { + case entry @ (previousIndex = None) => + Diff.Dynamic( + entry.index, + build(Seq.empty, entry.value.dynamicMods, trackUpdates) + ) + case (index, Some(previousIndex), _) => + Diff.IndexChange( + index, + previousIndex + ) + }, count = keysCount ) ) diff --git a/core/src/scalive/Dyn.scala b/core/src/scalive/Dyn.scala index 9e540a3..600a251 100644 --- a/core/src/scalive/Dyn.scala +++ b/core/src/scalive/Dyn.scala @@ -99,11 +99,14 @@ private class SplitVar[I, O, Key]( private val memoized: mutable.Map[Key, (Var[I], O)] = mutable.Map.empty + private var previousKeysToIndex: Map[Key, Int] = Map.empty + private var nonEmptySyncCount = 0 private[scalive] def sync(): Unit = parent.sync() if parent.changed then + previousKeysToIndex = memoized.keys.zipWithIndex.toMap // We keep track of the keys to remove deleted ones afterwards val nextKeys = mutable.HashSet.empty[Key] parent.currentValue.foreach(input => @@ -126,15 +129,25 @@ private class SplitVar[I, O, Key]( ) if memoized.nonEmpty then nonEmptySyncCount += 1 - private[scalive] def render(trackUpdates: Boolean) - : Option[(changeList: List[(Int, O)], keysCount: Int, includeStatics: Boolean)] = + private[scalive] def render(trackUpdates: Boolean): Option[ + ( + changeList: List[(index: Int, previousIndex: Option[Int], value: O)], + keysCount: Int, + includeStatics: Boolean + ) + ] = if parent.changed || !trackUpdates then Some( ( - changeList = memoized.values.zipWithIndex.collect { - case ((entryVar, output), index) if !trackUpdates || entryVar.changed => - (index, output) - }.toList, + changeList = memoized.zipWithIndex + .map { case ((key, (entryVar, output)), index) => + (index, previousKeysToIndex.get(key).filterNot(_ == index), entryVar, output) + } + .collect { + case (index, previousIndex, entryVar, output) + if !trackUpdates || entryVar.changed || previousIndex.isDefined => + (index, previousIndex, output) + }.toList, keysCount = memoized.size, includeStatics = nonEmptySyncCount == 1 ) diff --git a/core/src/scalive/HtmlBuilder.scala b/core/src/scalive/HtmlBuilder.scala index eef40b8..e92cdd7 100644 --- a/core/src/scalive/HtmlBuilder.scala +++ b/core/src/scalive/HtmlBuilder.scala @@ -36,10 +36,8 @@ object HtmlBuilder: case Content.DynElementColl(dyn) => ??? case Content.DynSplit(splitVar) => val (entries, _, _) = splitVar.render(false).getOrElse((List.empty, 0, true)) - val staticOpt = entries.collectFirst { case (_, el) => el.static } - entries.foreach((_, entryEl) => - build(staticOpt.getOrElse(Nil), entryEl.dynamicMods, strw) - ) + val staticOpt = entries.collectFirst { case (value = el) => el.static } + entries.foreach(entry => build(staticOpt.getOrElse(Nil), entry.value.dynamicMods, strw)) strw.write(static.last) end HtmlBuilder diff --git a/core/test/src/scalive/LiveViewSpec.scala b/core/test/src/scalive/LiveViewSpec.scala index a3bc2b5..f17e4f8 100644 --- a/core/test/src/scalive/LiveViewSpec.scala +++ b/core/test/src/scalive/LiveViewSpec.scala @@ -372,5 +372,183 @@ object LiveViewSpec extends TestSuite: } } + test("splitById mod") { + val initModel = TestModel( + items = List( + NestedModel("a", 10), + NestedModel("b", 15), + NestedModel("c", 20) + ) + ) + val model = Var(initModel) + val el = + div( + ul( + model(_.items).splitBy(_.name)((_, elem) => + li( + "Nom: ", + elem(_.name), + " Age: ", + elem(_.age.toString) + ) + ) + ) + ) + + el.syncAll() + el.setAllUnchanged() + + test("init") { + assertEqualsDiff( + el, + Json + .Obj( + "s" -> Json.Arr(Json.Str("