mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Fix splitById
This commit is contained in:
parent
21309629bc
commit
1da129f855
6 changed files with 247 additions and 40 deletions
|
|
@ -10,10 +10,10 @@ enum Diff:
|
||||||
dynamic: Seq[Diff.Dynamic] = Seq.empty)
|
dynamic: Seq[Diff.Dynamic] = Seq.empty)
|
||||||
case Comprehension(
|
case Comprehension(
|
||||||
static: Seq[String] = Seq.empty,
|
static: Seq[String] = Seq.empty,
|
||||||
entries: Seq[Diff.Dynamic] = Seq.empty,
|
entries: Seq[Diff.Dynamic | Diff.IndexChange] = Seq.empty,
|
||||||
count: Int = 0)
|
count: Int = 0)
|
||||||
case Value(value: String)
|
case Value(value: String)
|
||||||
case Dynamic(key: String, diff: Diff)
|
case Dynamic(index: Int, diff: Diff)
|
||||||
case Deleted
|
case Deleted
|
||||||
|
|
||||||
extension (diff: Diff)
|
extension (diff: Diff)
|
||||||
|
|
@ -27,6 +27,8 @@ extension (diff: Diff)
|
||||||
object Diff:
|
object Diff:
|
||||||
given JsonEncoder[Diff] = JsonEncoder[Json].contramap(toJson(_))
|
given JsonEncoder[Diff] = JsonEncoder[Json].contramap(toJson(_))
|
||||||
|
|
||||||
|
final case class IndexChange(index: Int, previousIndex: Int)
|
||||||
|
|
||||||
private def toJson(diff: Diff): Json =
|
private def toJson(diff: Diff): Json =
|
||||||
diff match
|
diff match
|
||||||
case Diff.Tag(static, dynamic) =>
|
case Diff.Tag(static, dynamic) =>
|
||||||
|
|
@ -35,7 +37,7 @@ object Diff:
|
||||||
.when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*))
|
.when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*))
|
||||||
.to(Chunk)
|
.to(Chunk)
|
||||||
.appendedAll(
|
.appendedAll(
|
||||||
dynamic.map(d => d.key -> toJson(d.diff))
|
dynamic.map(d => d.index.toString -> toJson(d.diff))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
case Diff.Comprehension(static, entries, count) =>
|
case Diff.Comprehension(static, entries, count) =>
|
||||||
|
|
@ -47,7 +49,12 @@ object Diff:
|
||||||
"k" ->
|
"k" ->
|
||||||
Json
|
Json
|
||||||
.Obj(
|
.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))
|
).add("kc", Json.Num(count))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ object DiffBuilder:
|
||||||
static = static,
|
static = static,
|
||||||
dynamic =
|
dynamic =
|
||||||
buildDynamic(dynamicMods, trackUpdates).zipWithIndex.collect { case (Some(diff), index) =>
|
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)) =>
|
case Some((entries, keysCount, includeStatics)) =>
|
||||||
val static =
|
val static =
|
||||||
if !trackUpdates || includeStatics then
|
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
|
else List.empty
|
||||||
List(
|
List(
|
||||||
Some(
|
Some(
|
||||||
Diff.Comprehension(
|
Diff.Comprehension(
|
||||||
static = static,
|
static = static,
|
||||||
entries = entries.map((key, el) =>
|
entries = entries.map {
|
||||||
Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates))
|
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
|
count = keysCount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,14 @@ private class SplitVar[I, O, Key](
|
||||||
private val memoized: mutable.Map[Key, (Var[I], O)] =
|
private val memoized: mutable.Map[Key, (Var[I], O)] =
|
||||||
mutable.Map.empty
|
mutable.Map.empty
|
||||||
|
|
||||||
|
private var previousKeysToIndex: Map[Key, Int] = Map.empty
|
||||||
|
|
||||||
private var nonEmptySyncCount = 0
|
private var nonEmptySyncCount = 0
|
||||||
|
|
||||||
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
|
||||||
// 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]
|
||||||
parent.currentValue.foreach(input =>
|
parent.currentValue.foreach(input =>
|
||||||
|
|
@ -126,15 +129,25 @@ private class SplitVar[I, O, Key](
|
||||||
)
|
)
|
||||||
if memoized.nonEmpty then nonEmptySyncCount += 1
|
if memoized.nonEmpty then nonEmptySyncCount += 1
|
||||||
|
|
||||||
private[scalive] def render(trackUpdates: Boolean)
|
private[scalive] def render(trackUpdates: Boolean): Option[
|
||||||
: Option[(changeList: List[(Int, O)], keysCount: Int, includeStatics: Boolean)] =
|
(
|
||||||
|
changeList: List[(index: Int, previousIndex: Option[Int], value: O)],
|
||||||
|
keysCount: Int,
|
||||||
|
includeStatics: Boolean
|
||||||
|
)
|
||||||
|
] =
|
||||||
if parent.changed || !trackUpdates then
|
if parent.changed || !trackUpdates then
|
||||||
Some(
|
Some(
|
||||||
(
|
(
|
||||||
changeList = memoized.values.zipWithIndex.collect {
|
changeList = memoized.zipWithIndex
|
||||||
case ((entryVar, output), index) if !trackUpdates || entryVar.changed =>
|
.map { case ((key, (entryVar, output)), index) =>
|
||||||
(index, output)
|
(index, previousKeysToIndex.get(key).filterNot(_ == index), entryVar, output)
|
||||||
}.toList,
|
}
|
||||||
|
.collect {
|
||||||
|
case (index, previousIndex, entryVar, output)
|
||||||
|
if !trackUpdates || entryVar.changed || previousIndex.isDefined =>
|
||||||
|
(index, previousIndex, output)
|
||||||
|
}.toList,
|
||||||
keysCount = memoized.size,
|
keysCount = memoized.size,
|
||||||
includeStatics = nonEmptySyncCount == 1
|
includeStatics = nonEmptySyncCount == 1
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,8 @@ object HtmlBuilder:
|
||||||
case Content.DynElementColl(dyn) => ???
|
case Content.DynElementColl(dyn) => ???
|
||||||
case Content.DynSplit(splitVar) =>
|
case Content.DynSplit(splitVar) =>
|
||||||
val (entries, _, _) = splitVar.render(false).getOrElse((List.empty, 0, true))
|
val (entries, _, _) = splitVar.render(false).getOrElse((List.empty, 0, true))
|
||||||
val staticOpt = entries.collectFirst { case (_, el) => el.static }
|
val staticOpt = entries.collectFirst { case (value = el) => el.static }
|
||||||
entries.foreach((_, entryEl) =>
|
entries.foreach(entry => build(staticOpt.getOrElse(Nil), entry.value.dynamicMods, strw))
|
||||||
build(staticOpt.getOrElse(Nil), entryEl.dynamicMods, strw)
|
|
||||||
)
|
|
||||||
strw.write(static.last)
|
strw.write(static.last)
|
||||||
|
|
||||||
end HtmlBuilder
|
end HtmlBuilder
|
||||||
|
|
|
||||||
|
|
@ -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("<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("a"),
|
||||||
|
"1" -> Json.Str("10")
|
||||||
|
),
|
||||||
|
"1" -> Json.Obj(
|
||||||
|
"0" -> Json.Str("b"),
|
||||||
|
"1" -> Json.Str("15")
|
||||||
|
),
|
||||||
|
"2" -> Json.Obj(
|
||||||
|
"0" -> Json.Str("c"),
|
||||||
|
"1" -> Json.Str("20")
|
||||||
|
),
|
||||||
|
"kc" -> Json.Num(3)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
trackChanges = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
test("diff no update") {
|
||||||
|
assertEqualsDiff(el, emptyDiff)
|
||||||
|
}
|
||||||
|
test("diff with unrelated update") {
|
||||||
|
model.update(_.copy(title = "title updated"))
|
||||||
|
assertEqualsDiff(el, emptyDiff)
|
||||||
|
}
|
||||||
|
test("diff with item changed") {
|
||||||
|
model.update(_.copy(items = initModel.items.updated(2, NestedModel("c", 99))))
|
||||||
|
assertEqualsDiff(
|
||||||
|
el,
|
||||||
|
Json.Obj(
|
||||||
|
"0" ->
|
||||||
|
Json
|
||||||
|
.Obj(
|
||||||
|
"k" -> Json.Obj(
|
||||||
|
"2" -> Json.Obj(
|
||||||
|
"1" -> Json.Str("99")
|
||||||
|
),
|
||||||
|
"kc" -> Json.Num(3)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
test("diff with item added") {
|
||||||
|
model.update(_.copy(items = initModel.items.appended(NestedModel("d", 35))))
|
||||||
|
assertEqualsDiff(
|
||||||
|
el,
|
||||||
|
Json.Obj(
|
||||||
|
"0" ->
|
||||||
|
Json
|
||||||
|
.Obj(
|
||||||
|
"k" -> Json.Obj(
|
||||||
|
"3" -> Json.Obj(
|
||||||
|
"0" -> Json.Str("d"),
|
||||||
|
"1" -> Json.Str("35")
|
||||||
|
),
|
||||||
|
"kc" -> Json.Num(4)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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("<li>Nom: "),
|
||||||
|
Json.Str(" Age: "),
|
||||||
|
Json.Str("</li>")
|
||||||
|
),
|
||||||
|
"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(
|
||||||
|
el,
|
||||||
|
Json.Obj(
|
||||||
|
"0" ->
|
||||||
|
Json
|
||||||
|
.Obj(
|
||||||
|
"k" -> Json.Obj(
|
||||||
|
"0" -> Json.Num(1),
|
||||||
|
"1" -> Json.Num(2),
|
||||||
|
"kc" -> Json.Num(2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
test("diff all removed") {
|
||||||
|
model.update(_.copy(items = List.empty))
|
||||||
|
assertEqualsDiff(
|
||||||
|
el,
|
||||||
|
Json.Obj(
|
||||||
|
"0" ->
|
||||||
|
Json
|
||||||
|
.Obj(
|
||||||
|
"k" -> Json.Obj(
|
||||||
|
"kc" -> Json.Num(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
end LiveViewSpec
|
end LiveViewSpec
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class TodoLiveView() extends LiveView[Msg, Model]:
|
||||||
|
|
||||||
def update(model: Model) =
|
def update(model: Model) =
|
||||||
case Msg.Add(text) =>
|
case Msg.Add(text) =>
|
||||||
val nextId = model.todos.maxByOption(_.id).map(_.id).getOrElse(1)
|
val nextId = model.todos.maxByOption(_.id).map(_.id).getOrElse(1) + 1
|
||||||
ZIO.succeed(
|
ZIO.succeed(
|
||||||
model
|
model
|
||||||
.focus(_.todos)
|
.focus(_.todos)
|
||||||
|
|
@ -37,35 +37,34 @@ class TodoLiveView() extends LiveView[Msg, Model]:
|
||||||
div(
|
div(
|
||||||
cls := "card-body",
|
cls := "card-body",
|
||||||
h1(cls := "card-title", "Todos"),
|
h1(cls := "card-title", "Todos"),
|
||||||
div(
|
|
||||||
cls := "flex items-center gap-3",
|
|
||||||
input(
|
|
||||||
cls := "input input-bordered grow",
|
|
||||||
typ := "text",
|
|
||||||
nameAttr := "todo-text",
|
|
||||||
placeholder := "What needs to be done?",
|
|
||||||
phx.onKeyup.withValue(Msg.Add(_)),
|
|
||||||
phx.key := "Enter",
|
|
||||||
phx.value("test") := "some value"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
form(
|
form(
|
||||||
cls := "flex items-center gap-3",
|
cls := "flex items-center gap-3",
|
||||||
phx.onChange(p => Msg.Add(p("todo-text"))),
|
phx.onSubmit(p => Msg.Add(p("todo-name"))),
|
||||||
phx.onSubmit(p => Msg.Add(p("todo-text"))),
|
|
||||||
input(
|
input(
|
||||||
cls := "input input-bordered grow",
|
cls := "input input-bordered grow",
|
||||||
typ := "text",
|
typ := "text",
|
||||||
nameAttr := "todo-text",
|
nameAttr := "todo-name",
|
||||||
placeholder := "What needs to be done?"
|
placeholder := "What needs to be done?",
|
||||||
|
value := model(_.inputText)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
ul(
|
ul(
|
||||||
cls := "divide-y divide-base-200",
|
cls := "divide-y divide-base-200",
|
||||||
model(_.todos).splitByIndex((_, elem) =>
|
model(_.todos).splitBy(_.id)((id, todo) =>
|
||||||
li(
|
li(
|
||||||
cls := "py-3 flex flex-wrap items-center justify-between gap-2",
|
cls := "py-3 flex items-center gap-3",
|
||||||
elem(_.text)
|
input(
|
||||||
|
tpe := "checkbox",
|
||||||
|
cls := "checkbox checkbox-primary",
|
||||||
|
checked := todo(_.completed),
|
||||||
|
phx.onClick(Msg.ToggleCompletion(id))
|
||||||
|
),
|
||||||
|
todo(_.text),
|
||||||
|
button(
|
||||||
|
cls := "btn btn-ghost btn-sm text-error",
|
||||||
|
phx.onClick(Msg.Remove(id)),
|
||||||
|
"✕"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -82,7 +81,10 @@ object TodoLiveView:
|
||||||
case Remove(id: Int)
|
case Remove(id: Int)
|
||||||
case ToggleCompletion(id: Int)
|
case ToggleCompletion(id: Int)
|
||||||
|
|
||||||
final case class Model(todos: List[Todo] = List.empty, filter: Filter = Filter.All)
|
final case class Model(
|
||||||
|
todos: List[Todo] = List.empty,
|
||||||
|
inputText: String = "",
|
||||||
|
filter: Filter = Filter.All)
|
||||||
final case class Todo(id: Int, text: String, completed: Boolean = false)
|
final case class Todo(id: Int, text: String, completed: Boolean = false)
|
||||||
enum Filter:
|
enum Filter:
|
||||||
case All, Active, Completed
|
case All, Active, Completed
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue