Complete TODO example

This commit is contained in:
Paul-Henri Froidmont 2025-11-07 04:50:03 +01:00
parent c19086ef32
commit bc113df11d
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
2 changed files with 100 additions and 22 deletions

View file

@ -37,7 +37,6 @@ sealed trait Dyn[T]:
private[scalive] def callOnEveryChild(f: T => Unit): Unit
extension [T](parent: Dyn[List[T]])
// TODO fix
def splitBy[Key](key: T => Key)(project: (Key, Dyn[T]) => HtmlElement): Mod =
Mod.Content.DynSplit(
new SplitVar(

View file

@ -1,38 +1,42 @@
import TodoLiveView.*
import monocle.syntax.all.*
import scalive.*
import zio.*
import zio.stream.ZStream
class TodoLiveView() extends LiveView[Msg, Model]:
def init = Model(List(Todo(99, "Buy eggs")))
def init = Model(
List(
Todo(99, "Buy eggs"),
Todo(1, "Wash dishes", true)
)
)
def update(model: Model) =
case Msg.Add(text) =>
val nextId = model.todos.maxByOption(_.id).map(_.id).getOrElse(1) + 1
model
.focus(_.todos)
.modify(_.appended(Todo(nextId, text)))
val nextId = model.items.maxByOption(_.id).map(_.id).getOrElse(1) + 1
model.copy(items = model.items.appended(Todo(nextId, text)))
case Msg.Edit(id) =>
model.updateItem(id, _.copy(editing = true))
case Msg.Update(id, text) =>
model.updateItem(id, _.copy(text = text, editing = false))
case Msg.Remove(id) =>
model
.focus(_.todos)
.modify(_.filterNot(_.id == id))
model.copy(items = model.items.filterNot(_.id == id))
case Msg.ToggleCompletion(id) =>
model
.focus(_.todos)
.modify(
_.map(todo => if todo.id == id then todo.copy(completed = todo.completed) else todo)
)
model.updateItem(id, todo => todo.copy(completed = !todo.completed))
case Msg.SetFilter(filter) =>
model.copy(filter = filter)
case Msg.RemoveCompleted =>
model.copy(items = model.items.filterNot(_.completed))
def view(model: Dyn[Model]) =
div(
cls := "mx-auto card bg-base-100 max-w-2xl shadow-xl space-y-6 p-6",
div(
cls := "card-body",
h1(cls := "card-title", "Todos"),
h1(cls := "card-title text-3xl", "Todos"),
form(
cls := "flex items-center gap-3",
cls := "mt-6 flex items-center gap-3",
phx.onSubmit(p => Msg.Add(p("todo-name"))),
input(
cls := "input input-bordered grow",
@ -44,7 +48,7 @@ class TodoLiveView() extends LiveView[Msg, Model]:
),
ul(
cls := "divide-y divide-base-200",
model(_.todos).splitBy(_.id)((id, todo) =>
model(_.filteredItems).splitBy(_.id)((id, todo) =>
li(
cls := "py-3 flex items-center gap-3",
input(
@ -53,7 +57,25 @@ class TodoLiveView() extends LiveView[Msg, Model]:
checked := todo(_.completed),
phx.onClick(Msg.ToggleCompletion(id))
),
todo(_.text),
todo.whenNot(_.editing)(
div(
cls := "truncate cursor-text w-full",
phx.onClick(Msg.Edit(id)),
span(
cls := todo(t => if t.completed then "line-through opacity-60" else ""),
todo(_.text)
)
)
),
todo.when(_.editing)(
input(
tpe := "text",
cls := "input input-bordered w-full",
value := todo(_.text),
phx.onMounted(JS.focus()),
phx.onBlur.withValue(Msg.Update(id, _))
)
),
button(
cls := "btn btn-ghost btn-sm text-error",
phx.onClick(Msg.Remove(id)),
@ -61,6 +83,43 @@ class TodoLiveView() extends LiveView[Msg, Model]:
)
)
)
),
div(
cls := "card-actions flex flex-wrap items-center gap-3 justify-between",
div(
cls := "join",
button(
cls := model(_.filter match
case Filter.All => "btn btn-sm join-item btn-active"
case _ => "btn btn-sm join-item"),
phx.onClick(Msg.SetFilter(Filter.All)),
"All"
),
button(
cls := model(_.filter match
case Filter.Active => "btn btn-sm join-item btn-active"
case _ => "btn btn-sm join-item"),
phx.onClick(Msg.SetFilter(Filter.Active)),
"Active"
),
button(
cls := model(_.filter match
case Filter.Completed => "btn btn-sm join-item btn-active"
case _ => "btn btn-sm join-item"),
phx.onClick(Msg.SetFilter(Filter.Completed)),
"Completed"
)
),
span(
cls := "badge badge-outline",
model(m => s"${m.itemsLeft} item${if m.itemsLeft == 1 then "" else "s"} left")
),
button(
cls := "btn btn-sm btn-outline",
disabled := model(_.completedCount == 0),
phx.onClick(Msg.RemoveCompleted),
model(m => s"Clear completed (${m.completedCount})")
)
)
)
)
@ -72,13 +131,33 @@ object TodoLiveView:
enum Msg:
case Add(text: String)
case Edit(id: Int)
case Update(id: Int, text: String)
case Remove(id: Int)
case ToggleCompletion(id: Int)
case SetFilter(value: Filter)
case RemoveCompleted
final case class Model(
todos: List[Todo] = List.empty,
items: List[Todo] = List.empty,
inputText: String = "",
filter: Filter = Filter.All)
final case class Todo(id: Int, text: String, completed: Boolean = false)
filter: Filter = Filter.All):
def itemsLeft = items.count(!_.completed)
def completedCount = items.count(_.completed)
def filteredItems = items.filter(item =>
filter match
case Filter.All => true
case Filter.Active => !item.completed
case Filter.Completed => item.completed
)
def updateItem(id: Int, f: Todo => Todo) =
import monocle.syntax.all.*
this
.focus(_.items).each.filter(_.id == id)
.modify(f)
final case class Todo(id: Int, text: String, completed: Boolean = false, editing: Boolean = false)
enum Filter:
case All, Active, Completed
end TodoLiveView