Fix splitBy ordering, properly this time

This commit is contained in:
Paul-Henri Froidmont 2025-12-07 00:55:36 +01:00
parent d011477d9e
commit 4c9dfb2533
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
2 changed files with 95 additions and 5 deletions

View file

@ -37,7 +37,7 @@ sealed trait Dyn[T]:
private[scalive] def callOnEveryChild(f: T => Unit): Unit private[scalive] def callOnEveryChild(f: T => Unit): Unit
extension [T](parent: Dyn[List[T]]) 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( Mod.Content.DynSplit(
new SplitVar( new SplitVar(
parent, 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[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]], parent: Dyn[List[I]],
key: I => Key, key: I => Key,
project: (Key, Dyn[I]) => O): project: (Key, Dyn[I]) => O):
private val memoized: mutable.Map[Key, (Var[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 private var previousKeysToIndex: Map[Key, Int] = Map.empty
@ -105,9 +107,10 @@ private class SplitVar[I, O, Key: Ordering](
private[scalive] def sync(): Unit = private[scalive] def sync(): Unit =
parent.sync() parent.sync()
if parent.changed then if parent.changed then
previousKeysToIndex = memoized.keys.zipWithIndex.toMap previousKeysToIndex = orderedKeys.zipWithIndex.toMap
// We keep track of the keys to remove deleted ones afterwards // We keep track of the keys to remove deleted ones afterwards
val nextKeys = mutable.HashSet.empty[Key] val nextKeys = mutable.HashSet.empty[Key]
orderedKeys = parent.currentValue.map(key)
parent.currentValue.foreach(input => parent.currentValue.foreach(input =>
val entryKey = key(input) val entryKey = key(input)
nextKeys += entryKey nextKeys += entryKey
@ -138,7 +141,8 @@ private class SplitVar[I, O, Key: Ordering](
if parent.changed || !trackUpdates then if parent.changed || !trackUpdates then
Some( Some(
( (
changeList = memoized.zipWithIndex changeList = orderedKeys
.map(k => (k, memoized(k))).zipWithIndex
.map { case ((key, (entryVar, output)), index) => .map { case ((key, (entryVar, output)), index) =>
(index, previousKeysToIndex.get(key).filterNot(_ == index), entryVar, output) (index, previousKeysToIndex.get(key).filterNot(_ == index), entryVar, output)
} }

View file

@ -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("<div><ul>"), Json.Str("</ul></div>")),
"0" -> Json.Obj(
"s" -> Json.Arr(
Json.Str("<li>Nom: "),
Json.Str(" Age: "),
Json.Str("</li>")
),
"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 end LiveViewSpec