From eab5eb331652aa1f58758836758d473fad6bfced Mon Sep 17 00:00:00 2001 From: Paul-Henri Froidmont Date: Thu, 25 Sep 2025 03:37:36 +0200 Subject: [PATCH] Fix splitByIndex diff --- core/src/scalive/DiffBuilder.scala | 17 +++++----- core/src/scalive/Dyn.scala | 15 ++++++--- core/src/scalive/HtmlBuilder.scala | 12 +++---- core/test/src/scalive/LiveViewSpec.scala | 42 ++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/core/src/scalive/DiffBuilder.scala b/core/src/scalive/DiffBuilder.scala index affd773..7337757 100644 --- a/core/src/scalive/DiffBuilder.scala +++ b/core/src/scalive/DiffBuilder.scala @@ -41,19 +41,18 @@ object DiffBuilder: case Content.DynElementColl(dyn) => ??? case Content.DynSplit(splitVar) => splitVar.render(trackUpdates) match - case Some((entries, keysCount)) => + case Some((entries, keysCount, includeStatics)) => val static = - entries.collectFirst { case (_, Some(el)) => el.static }.getOrElse(List.empty) - val includeStatic = !trackUpdates || keysCount == entries.count(_._2.isDefined) + if !trackUpdates || includeStatics then + entries.collectFirst { case (_, el) => el.static }.getOrElse(List.empty) + else List.empty List( Some( Diff.Comprehension( - static = if includeStatic then static else Seq.empty, - entries = entries.map { - case (key, Some(el)) => - Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates)) - case (key, None) => Diff.Dynamic(key.toString, Diff.Deleted) - }, + static = static, + entries = entries.map((key, el) => + Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates)) + ), count = keysCount ) ) diff --git a/core/src/scalive/Dyn.scala b/core/src/scalive/Dyn.scala index 41ced5a..9e540a3 100644 --- a/core/src/scalive/Dyn.scala +++ b/core/src/scalive/Dyn.scala @@ -99,10 +99,12 @@ private class SplitVar[I, O, Key]( private val memoized: mutable.Map[Key, (Var[I], O)] = mutable.Map.empty + private var nonEmptySyncCount = 0 + private[scalive] def sync(): Unit = parent.sync() if parent.changed then - // We keep track of the key to set deleted ones to None + // We keep track of the keys to remove deleted ones afterwards val nextKeys = mutable.HashSet.empty[Key] parent.currentValue.foreach(input => val entryKey = key(input) @@ -122,16 +124,19 @@ private class SplitVar[I, O, Key]( if !nextKeys.contains(k) then val _ = memoized.remove(k) ) + if memoized.nonEmpty then nonEmptySyncCount += 1 private[scalive] def render(trackUpdates: Boolean) - : Option[(changeList: List[(Key, Option[O])], keysCount: Int)] = + : Option[(changeList: List[(Int, O)], keysCount: Int, includeStatics: Boolean)] = if parent.changed || !trackUpdates then Some( ( - memoized.collect { - case (k, (entryVar, output)) if !trackUpdates || entryVar.changed => (k, Some(output)) + changeList = memoized.values.zipWithIndex.collect { + case ((entryVar, output), index) if !trackUpdates || entryVar.changed => + (index, output) }.toList, - memoized.size + keysCount = memoized.size, + includeStatics = nonEmptySyncCount == 1 ) ) else None diff --git a/core/src/scalive/HtmlBuilder.scala b/core/src/scalive/HtmlBuilder.scala index 7a8251d..1e551b2 100644 --- a/core/src/scalive/HtmlBuilder.scala +++ b/core/src/scalive/HtmlBuilder.scala @@ -33,13 +33,11 @@ object HtmlBuilder: dyn.render(false).foreach(_.foreach(el => build(el.static, el.dynamicMods, strw))) case Content.DynElementColl(dyn) => ??? case Content.DynSplit(splitVar) => - val (entries, _) = splitVar.render(false).getOrElse(List.empty -> 0) - val staticOpt = entries.collectFirst { case (_, Some(el)) => el.static } - entries.foreach { - case (_, Some(entryEl)) => - build(staticOpt.getOrElse(Nil), entryEl.dynamicMods, strw) - case _ => () - } + 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) + ) strw.write(static.last) end HtmlBuilder diff --git a/core/test/src/scalive/LiveViewSpec.scala b/core/test/src/scalive/LiveViewSpec.scala index bb2a5eb..a3bc2b5 100644 --- a/core/test/src/scalive/LiveViewSpec.scala +++ b/core/test/src/scalive/LiveViewSpec.scala @@ -290,6 +290,48 @@ object LiveViewSpec extends TestSuite: ) ) } + test("diff add one to empty list") { + val model = Var(TestModel(items = List.empty)) + val el = + div( + ul( + model(_.items).splitByIndex((_, elem) => + li( + "Nom: ", + elem(_.name), + " Age: ", + elem(_.age.toString) + ) + ) + ) + ) + el.syncAll() + el.setAllUnchanged() + + model.update(_.copy(items = List(NestedModel("a", 20)))) + + assertEqualsDiff( + el, + Json.Obj( + "0" -> + Json + .Obj( + "s" -> Json.Arr( + Json.Str("
  • Nom: "), + Json.Str(" Age: "), + Json.Str("
  • ") + ), + "k" -> Json.Obj( + "0" -> Json.Obj( + "0" -> Json.Str("a"), + "1" -> Json.Str("20") + ), + "kc" -> Json.Num(1) + ) + ) + ) + ) + } test("diff with first item removed") { model.update(_.copy(items = initModel.items.tail)) assertEqualsDiff(