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)
|
||||
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))
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class TodoLiveView() extends LiveView[Msg, Model]:
|
|||
|
||||
def update(model: Model) =
|
||||
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(
|
||||
model
|
||||
.focus(_.todos)
|
||||
|
|
@ -37,35 +37,34 @@ class TodoLiveView() extends LiveView[Msg, Model]:
|
|||
div(
|
||||
cls := "card-body",
|
||||
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(
|
||||
cls := "flex items-center gap-3",
|
||||
phx.onChange(p => Msg.Add(p("todo-text"))),
|
||||
phx.onSubmit(p => Msg.Add(p("todo-text"))),
|
||||
phx.onSubmit(p => Msg.Add(p("todo-name"))),
|
||||
input(
|
||||
cls := "input input-bordered grow",
|
||||
typ := "text",
|
||||
nameAttr := "todo-text",
|
||||
placeholder := "What needs to be done?"
|
||||
nameAttr := "todo-name",
|
||||
placeholder := "What needs to be done?",
|
||||
value := model(_.inputText)
|
||||
)
|
||||
),
|
||||
ul(
|
||||
cls := "divide-y divide-base-200",
|
||||
model(_.todos).splitByIndex((_, elem) =>
|
||||
model(_.todos).splitBy(_.id)((id, todo) =>
|
||||
li(
|
||||
cls := "py-3 flex flex-wrap items-center justify-between gap-2",
|
||||
elem(_.text)
|
||||
cls := "py-3 flex items-center gap-3",
|
||||
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 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)
|
||||
enum Filter:
|
||||
case All, Active, Completed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue