mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Complete TODO example
This commit is contained in:
parent
c19086ef32
commit
bc113df11d
2 changed files with 100 additions and 22 deletions
|
|
@ -37,7 +37,6 @@ 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]])
|
||||||
// TODO fix
|
|
||||||
def splitBy[Key](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(
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,42 @@
|
||||||
import TodoLiveView.*
|
import TodoLiveView.*
|
||||||
import monocle.syntax.all.*
|
|
||||||
import scalive.*
|
import scalive.*
|
||||||
import zio.*
|
import zio.*
|
||||||
import zio.stream.ZStream
|
import zio.stream.ZStream
|
||||||
|
|
||||||
class TodoLiveView() extends LiveView[Msg, Model]:
|
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) =
|
def update(model: Model) =
|
||||||
case Msg.Add(text) =>
|
case Msg.Add(text) =>
|
||||||
val nextId = model.todos.maxByOption(_.id).map(_.id).getOrElse(1) + 1
|
val nextId = model.items.maxByOption(_.id).map(_.id).getOrElse(1) + 1
|
||||||
model
|
model.copy(items = model.items.appended(Todo(nextId, text)))
|
||||||
.focus(_.todos)
|
case Msg.Edit(id) =>
|
||||||
.modify(_.appended(Todo(nextId, text)))
|
model.updateItem(id, _.copy(editing = true))
|
||||||
|
case Msg.Update(id, text) =>
|
||||||
|
model.updateItem(id, _.copy(text = text, editing = false))
|
||||||
case Msg.Remove(id) =>
|
case Msg.Remove(id) =>
|
||||||
model
|
model.copy(items = model.items.filterNot(_.id == id))
|
||||||
.focus(_.todos)
|
|
||||||
.modify(_.filterNot(_.id == id))
|
|
||||||
case Msg.ToggleCompletion(id) =>
|
case Msg.ToggleCompletion(id) =>
|
||||||
model
|
model.updateItem(id, todo => todo.copy(completed = !todo.completed))
|
||||||
.focus(_.todos)
|
case Msg.SetFilter(filter) =>
|
||||||
.modify(
|
model.copy(filter = filter)
|
||||||
_.map(todo => if todo.id == id then todo.copy(completed = todo.completed) else todo)
|
case Msg.RemoveCompleted =>
|
||||||
)
|
model.copy(items = model.items.filterNot(_.completed))
|
||||||
|
|
||||||
def view(model: Dyn[Model]) =
|
def view(model: Dyn[Model]) =
|
||||||
div(
|
div(
|
||||||
cls := "mx-auto card bg-base-100 max-w-2xl shadow-xl space-y-6 p-6",
|
cls := "mx-auto card bg-base-100 max-w-2xl shadow-xl space-y-6 p-6",
|
||||||
div(
|
div(
|
||||||
cls := "card-body",
|
cls := "card-body",
|
||||||
h1(cls := "card-title", "Todos"),
|
h1(cls := "card-title text-3xl", "Todos"),
|
||||||
form(
|
form(
|
||||||
cls := "flex items-center gap-3",
|
cls := "mt-6 flex items-center gap-3",
|
||||||
phx.onSubmit(p => Msg.Add(p("todo-name"))),
|
phx.onSubmit(p => Msg.Add(p("todo-name"))),
|
||||||
input(
|
input(
|
||||||
cls := "input input-bordered grow",
|
cls := "input input-bordered grow",
|
||||||
|
|
@ -44,7 +48,7 @@ class TodoLiveView() extends LiveView[Msg, Model]:
|
||||||
),
|
),
|
||||||
ul(
|
ul(
|
||||||
cls := "divide-y divide-base-200",
|
cls := "divide-y divide-base-200",
|
||||||
model(_.todos).splitBy(_.id)((id, todo) =>
|
model(_.filteredItems).splitBy(_.id)((id, todo) =>
|
||||||
li(
|
li(
|
||||||
cls := "py-3 flex items-center gap-3",
|
cls := "py-3 flex items-center gap-3",
|
||||||
input(
|
input(
|
||||||
|
|
@ -53,7 +57,25 @@ class TodoLiveView() extends LiveView[Msg, Model]:
|
||||||
checked := todo(_.completed),
|
checked := todo(_.completed),
|
||||||
phx.onClick(Msg.ToggleCompletion(id))
|
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(
|
button(
|
||||||
cls := "btn btn-ghost btn-sm text-error",
|
cls := "btn btn-ghost btn-sm text-error",
|
||||||
phx.onClick(Msg.Remove(id)),
|
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:
|
enum Msg:
|
||||||
case Add(text: String)
|
case Add(text: String)
|
||||||
|
case Edit(id: Int)
|
||||||
|
case Update(id: Int, text: String)
|
||||||
case Remove(id: Int)
|
case Remove(id: Int)
|
||||||
case ToggleCompletion(id: Int)
|
case ToggleCompletion(id: Int)
|
||||||
|
case SetFilter(value: Filter)
|
||||||
|
case RemoveCompleted
|
||||||
|
|
||||||
final case class Model(
|
final case class Model(
|
||||||
todos: List[Todo] = List.empty,
|
items: List[Todo] = List.empty,
|
||||||
inputText: String = "",
|
inputText: String = "",
|
||||||
filter: Filter = Filter.All)
|
filter: Filter = Filter.All):
|
||||||
final case class Todo(id: Int, text: String, completed: Boolean = false)
|
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:
|
enum Filter:
|
||||||
case All, Active, Completed
|
case All, Active, Completed
|
||||||
|
end TodoLiveView
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue