From 4c9dfb253382a5f7a88049146c080a83ab0fe85d Mon Sep 17 00:00:00 2001 From: Paul-Henri Froidmont Date: Sun, 7 Dec 2025 00:55:36 +0100 Subject: [PATCH] Fix splitBy ordering, properly this time --- scalive/core/src/scalive/Dyn.scala | 14 +-- .../core/test/src/scalive/LiveViewSpec.scala | 86 +++++++++++++++++++ 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/scalive/core/src/scalive/Dyn.scala b/scalive/core/src/scalive/Dyn.scala index 4d474d6..4e42bd2 100644 --- a/scalive/core/src/scalive/Dyn.scala +++ b/scalive/core/src/scalive/Dyn.scala @@ -37,7 +37,7 @@ sealed trait Dyn[T]: private[scalive] def callOnEveryChild(f: T => Unit): Unit extension [T](parent: Dyn[List[T]]) - def splitBy[Key: Ordering](key: T => Key)(project: (Key, Dyn[T]) => HtmlElement): Mod = + def splitBy[Key](key: T => Key)(project: (Key, Dyn[T]) => HtmlElement): Mod = Mod.Content.DynSplit( new SplitVar( parent, @@ -90,13 +90,15 @@ private class DerivedVar[I, O] private[scalive] (parent: Var[I], f: I => O) exte private[scalive] def callOnEveryChild(f: O => Unit): Unit = f(currentValue) -private class SplitVar[I, O, Key: Ordering]( +private class SplitVar[I, O, Key]( parent: Dyn[List[I]], key: I => Key, project: (Key, Dyn[I]) => O): private val memoized: mutable.Map[Key, (Var[I], O)] = - mutable.TreeMap.empty + mutable.Map.empty + + private var orderedKeys = List.empty[Key] private var previousKeysToIndex: Map[Key, Int] = Map.empty @@ -105,9 +107,10 @@ private class SplitVar[I, O, Key: Ordering]( private[scalive] def sync(): Unit = parent.sync() if parent.changed then - previousKeysToIndex = memoized.keys.zipWithIndex.toMap + previousKeysToIndex = orderedKeys.zipWithIndex.toMap // We keep track of the keys to remove deleted ones afterwards val nextKeys = mutable.HashSet.empty[Key] + orderedKeys = parent.currentValue.map(key) parent.currentValue.foreach(input => val entryKey = key(input) nextKeys += entryKey @@ -138,7 +141,8 @@ private class SplitVar[I, O, Key: Ordering]( if parent.changed || !trackUpdates then Some( ( - changeList = memoized.zipWithIndex + changeList = orderedKeys + .map(k => (k, memoized(k))).zipWithIndex .map { case ((key, (entryVar, output)), index) => (index, previousKeysToIndex.get(key).filterNot(_ == index), entryVar, output) } diff --git a/scalive/core/test/src/scalive/LiveViewSpec.scala b/scalive/core/test/src/scalive/LiveViewSpec.scala index f17e4f8..263072a 100644 --- a/scalive/core/test/src/scalive/LiveViewSpec.scala +++ b/scalive/core/test/src/scalive/LiveViewSpec.scala @@ -550,5 +550,91 @@ object LiveViewSpec extends TestSuite: } } + test("splitBy ordering") { + val initModel = TestModel( + items = List( + NestedModel("c", 20), + NestedModel("a", 10), + NestedModel("b", 15) + ) + ) + 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 should preserve list order") { + assertEqualsDiff( + el, + Json + .Obj( + "s" -> Json.Arr(Json.Str("
")), + "0" -> Json.Obj( + "s" -> Json.Arr( + Json.Str("
  • Nom: "), + Json.Str(" Age: "), + Json.Str("
  • ") + ), + "k" -> Json.Obj( + "0" -> Json.Obj( + "0" -> Json.Str("c"), + "1" -> Json.Str("20") + ), + "1" -> Json.Obj( + "0" -> Json.Str("a"), + "1" -> Json.Str("10") + ), + "2" -> Json.Obj( + "0" -> Json.Str("b"), + "1" -> Json.Str("15") + ), + "kc" -> Json.Num(3) + ) + ) + ), + trackChanges = false + ) + } + + test("reorder items") { + model.update( + _.copy(items = + List( + NestedModel("b", 15), // Should be at index 0 + NestedModel("a", 10), // Should be at index 1 + NestedModel("c", 20) // Should be at index 2 + ) + ) + ) + assertEqualsDiff( + el, + Json.Obj( + "0" -> + Json + .Obj( + "k" -> Json.Obj( + "0" -> Json.Num(2), + "2" -> Json.Num(0), + "kc" -> Json.Num(3) + ) + ) + ) + ) + } + } + } end LiveViewSpec