Commit 7c9d868c authored by Øyvind Raddum Berg's avatar Øyvind Raddum Berg

scalajs-react example: Update to new API

parent b2c313e2
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -5,7 +5,7 @@ import org.scalajs.sbtplugin.ScalaJSPlugin ...@@ -5,7 +5,7 @@ import org.scalajs.sbtplugin.ScalaJSPlugin
import ScalaJSPlugin.autoImport._ import ScalaJSPlugin.autoImport._
object Build extends Build { object Build extends Build {
val scalaJsReactVersion = "0.9.1" val scalaJsReactVersion = "0.10.0"
val generatedDir = file("generated") val generatedDir = file("generated")
val todomvc = Project("todomvc", file(".")) val todomvc = Project("todomvc", file("."))
...@@ -15,7 +15,7 @@ object Build extends Build { ...@@ -15,7 +15,7 @@ object Build extends Build {
version := "1", version := "1",
licenses += ("Apache-2.0", url("http://opensource.org/licenses/Apache-2.0")), licenses += ("Apache-2.0", url("http://opensource.org/licenses/Apache-2.0")),
scalaVersion := "2.11.7", scalaVersion := "2.11.7",
scalacOptions ++= Seq("-deprecation", "-encoding", "UTF-8", "-feature", "-unchecked", "-Xlint", "-Yno-adapted-args", "-Ywarn-dead-code" ), scalacOptions ++= Seq("-deprecation", "-encoding", "UTF-8", "-feature", "-unchecked", "-Xlint", "-Yno-adapted-args", "-Ywarn-dead-code", "-Ywarn-value-discard" ),
updateOptions := updateOptions.value.withCachedResolution(true), updateOptions := updateOptions.value.withCachedResolution(true),
sbt.Keys.test in Test := (), sbt.Keys.test in Test := (),
emitSourceMaps := true, emitSourceMaps := true,
...@@ -23,14 +23,15 @@ object Build extends Build { ...@@ -23,14 +23,15 @@ object Build extends Build {
persistLauncher := true, persistLauncher := true,
/* javascript dependencies */ /* javascript dependencies */
jsDependencies += "org.webjars" % "react" % "0.12.1" / jsDependencies ++= Seq(
"react-with-addons.js" commonJSName "React" minified "react-with-addons.min.js", "org.webjars.npm" % "react" % "0.14.0" / "react-with-addons.js" commonJSName "React" minified "react-with-addons.min.js",
"org.webjars.npm" % "react-dom" % "0.14.0" / "react-dom.js" commonJSName "ReactDOM" minified "react-dom.min.js" dependsOn "react-with-addons.js"
),
/* scala.js dependencies */ /* scala.js dependencies */
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"com.github.japgolly.scalajs-react" %%% "ext-scalaz71" % scalaJsReactVersion,
"com.github.japgolly.scalajs-react" %%% "extra" % scalaJsReactVersion, "com.github.japgolly.scalajs-react" %%% "extra" % scalaJsReactVersion,
"com.lihaoyi" %%% "upickle" % "0.3.0" "com.lihaoyi" %%% "upickle" % "0.3.5"
), ),
/* move these files out of target/. Also sets up same file for both fast and full optimization */ /* move these files out of target/. Also sets up same file for both fast and full optimization */
......
package todomvc package todomvc
import japgolly.scalajs.react.ScalazReact._
import japgolly.scalajs.react._ import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._ import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom.html
import scalaz.effect.IO
import scalaz.syntax.std.list._
import scalaz.syntax.std.string._
object CFooter { object CFooter {
case class Props private[CFooter]( case class Props(
filterLink: TodoFilter => ReactTag, filterLink: TodoFilter => ReactTag,
onClearCompleted: IO[Unit], onClearCompleted: Callback,
currentFilter: TodoFilter, currentFilter: TodoFilter,
activeCount: Int, activeCount: Int,
completedCount: Int completedCount: Int
) )
case class Backend($: BackendScope[Props, Unit]) { class Backend($: BackendScope[Props, Unit]) {
val clearButton = def clearButton(P: Props): ReactTagOf[html.Button] =
<.button(^.className := "clear-completed", ^.onClick ~~> $.props.onClearCompleted, "Clear completed") <.button(
^.className := "clear-completed",
def filterLink(s: TodoFilter) = ^.onClick --> P.onClearCompleted,
<.li($.props.filterLink(s)(($.props.currentFilter == s) ?= (^.className := "selected"), s.title)) "Clear completed",
(P.completedCount == 0) ?= ^.visibility.hidden
)
def withSpaces(ts: TagMod*) = def filterLink(P: Props)(s: TodoFilter): ReactTagOf[html.LI] =
ts.toList.intersperse(" ") <.li(P.filterLink(s)((P.currentFilter == s) ?= (^.className := "selected"), s.title))
def render = def render(P: Props): ReactTagOf[html.Element] =
<.footer( <.footer(
^.className := "footer", ^.className := "footer",
<.span( <.span(
^.className := "todo-count", ^.className := "todo-count",
withSpaces(<.strong($.props.activeCount), "item" plural $.props.activeCount, "left") <.strong(P.activeCount),
s" ${if (P.activeCount == 1) "item" else "items"} left"
), ),
<.ul( <.ul(
^.className := "filters", ^.className := "filters",
withSpaces(TodoFilter.values map filterLink) TodoFilter.values map filterLink(P)
), ),
clearButton(($.props.completedCount == 0) ?= ^.visibility.hidden) clearButton(P)
) )
} }
private val component = ReactComponentB[Props]("CFooter") private val component = ReactComponentB[Props]("CFooter")
.stateless .stateless
.backend(Backend) .renderBackend[Backend]
.render(_.backend.render)
.build .build
def apply(filterLink: TodoFilter => ReactTag, def apply(P: Props) = component(P)
onClearCompleted:IO[Unit],
currentFilter: TodoFilter,
activeCount: Int,
completedCount: Int) =
component(
Props(
filterLink = filterLink,
onClearCompleted = onClearCompleted,
currentFilter = currentFilter,
activeCount = activeCount,
completedCount = completedCount
)
)
} }
package todomvc package todomvc
import japgolly.scalajs.react.ScalazReact._
import japgolly.scalajs.react._ import japgolly.scalajs.react._
import japgolly.scalajs.react.extra.{Reusability, Px}
import japgolly.scalajs.react.vdom.prefix_<^._ import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom.ext.KeyCode import org.scalajs.dom.ext.KeyCode
import scalaz.effect.IO
import scalaz.syntax.semigroup._
import scalaz.syntax.std.option._
import scalaz.std.anyVal.unitInstance
object CTodoItem { object CTodoItem {
case class Props private[CTodoItem] ( case class Props (
onToggle: IO[Unit], onToggle: Callback,
onDelete: IO[Unit], onDelete: Callback,
onStartEditing: IO[Unit], onStartEditing: Callback,
onUpdateTitle: Title => IO[Unit], onUpdateTitle: Title => Callback,
onCancelEditing: IO[Unit], onCancelEditing: Callback,
todo: Todo, todo: Todo,
isEditing: Boolean isEditing: Boolean
) )
case class State(editText: UnfinishedTitle) implicit val reusableProps = Reusability.fn[Props]((p1, p2) =>
(p1.todo eq p2.todo) && (p1.isEditing == p2.isEditing)
)
case class Backend($: BackendScope[Props, State]) { case class State(editText: UnfinishedTitle)
def editFieldSubmit: IO[Unit] = class Backend($: BackendScope[Props, State]) {
$.state.editText.validated.fold($.props.onDelete)($.props.onUpdateTitle) case class Callbacks(P: Props) {
val editFieldSubmit: Callback =
$.state.flatMap(_.editText.validated.fold(P.onDelete)(P.onUpdateTitle))
/** val resetText: Callback =
* It's OK to make these into `val`s as long as they don't touch state. $.modState(_.copy(editText = P.todo.title.editable))
*/
val resetText: IO[Unit] =
$.modStateIO(_.copy(editText = $.props.todo.title.editable))
val editFieldKeyDown: ReactKeyboardEvent => Option[IO[Unit]] = val editFieldKeyDown: ReactKeyboardEvent => Option[Callback] =
e => e.nativeEvent.keyCode match { e => e.nativeEvent.keyCode match {
case KeyCode.Escape => (resetText |+| $.props.onCancelEditing).some case KeyCode.Escape => Some(resetText >> P.onCancelEditing)
case KeyCode.Enter => editFieldSubmit.some case KeyCode.Enter => Some(editFieldSubmit)
case _ => None case _ => None
} }
}
val cbs = Px.cbA($.props).map(Callbacks)
val editFieldChanged: ReactEventI => IO[Unit] = val editFieldChanged: ReactEventI => Callback =
e => $.modStateIO(_.copy(editText = UnfinishedTitle(e.target.value))) e => $.modState(_.copy(editText = UnfinishedTitle(e.target.value)))
def render: ReactElement = { def render(P: Props, S: State): ReactElement = {
val cb = cbs.value()
<.li( <.li(
^.classSet( ^.classSet(
"completed" -> $.props.todo.isCompleted, "completed" -> P.todo.isCompleted,
"editing" -> $.props.isEditing "editing" -> P.isEditing
), ),
<.div( <.div(
^.className := "view", ^.className := "view",
<.input( <.input(
^.className := "toggle", ^.className := "toggle",
^.`type` := "checkbox", ^.`type` := "checkbox",
^.checked := $.props.todo.isCompleted, ^.checked := P.todo.isCompleted,
^.onChange ~~> $.props.onToggle ^.onChange --> P.onToggle
), ),
<.label($.props.todo.title.value, ^.onDoubleClick ~~> $.props.onStartEditing), <.label(
<.button(^.className := "destroy", ^.onClick ~~> $.props.onDelete) P.todo.title.value,
^.onDoubleClick --> P.onStartEditing
),
<.button(
^.className := "destroy",
^.onClick --> P.onDelete
)
), ),
<.input( <.input(
^.className := "edit", ^.className := "edit",
^.onBlur ~~> editFieldSubmit, ^.onBlur --> cb.editFieldSubmit,
^.onChange ~~> editFieldChanged, ^.onChange ==> editFieldChanged,
^.onKeyDown ~~>? editFieldKeyDown, ^.onKeyDown ==>? cb.editFieldKeyDown,
^.value := $.state.editText.value ^.value := S.editText.value
) )
) )
} }
} }
private val component = ReactComponentB[Props]("CTodoItem") val component = ReactComponentB[Props]("CTodoItem")
.initialStateP(p => State(p.todo.title.editable)) .initialState_P(p => State(p.todo.title.editable))
.backend(Backend) .renderBackend[Backend].build
.render(_.backend.render)
.build
def apply(onToggle: IO[Unit],
onDelete: IO[Unit],
onStartEditing: IO[Unit],
onUpdateTitle: Title => IO[Unit],
onCancelEditing: IO[Unit],
todo: Todo,
isEditing: Boolean) =
component.withKey(todo.id.id.toString)( def apply(P: Props) =
Props( component.withKey(P.todo.id.id.toString)(P)
onToggle = onToggle,
onDelete = onDelete,
onStartEditing = onStartEditing,
onUpdateTitle = onUpdateTitle,
onCancelEditing = onCancelEditing,
todo = todo,
isEditing = isEditing
)
)
} }
package todomvc package todomvc
import japgolly.scalajs.react.ScalazReact._
import japgolly.scalajs.react._ import japgolly.scalajs.react._
import japgolly.scalajs.react.extra._ import japgolly.scalajs.react.extra._
import japgolly.scalajs.react.extra.router2.RouterCtl import japgolly.scalajs.react.extra.router.RouterCtl
import japgolly.scalajs.react.vdom.prefix_<^._ import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom.ext.KeyCode import org.scalajs.dom.ext.KeyCode
import org.scalajs.dom.html
import scala.scalajs.js
import scalaz.effect.IO
import scalaz.std.anyVal.unitInstance
import scalaz.syntax.semigroup._
object CTodoList { object CTodoList {
case class Props private[CTodoList] (ctl: RouterCtl[TodoFilter], model: TodoModel, currentFilter: TodoFilter) case class Props private[CTodoList] (
ctl: RouterCtl[TodoFilter],
model: TodoModel,
currentFilter: TodoFilter
)
case class State(todos: Seq[Todo], editing: Option[TodoId]) case class State(
todos: Seq[Todo],
editing: Option[TodoId]
)
/** /**
* These specify when it makes sense to skip updating this component (see comment on `Listenable` below) * These specify when it makes sense to skip updating this component (see comment on `Listenable` below)
...@@ -31,38 +33,55 @@ object CTodoList { ...@@ -31,38 +33,55 @@ object CTodoList {
* *
* It extends OnUnmount so unsubscription of events can be made automatically. * It extends OnUnmount so unsubscription of events can be made automatically.
*/ */
case class Backend($: BackendScope[Props, State]) extends OnUnmount { class Backend($: BackendScope[Props, State]) extends OnUnmount {
def handleNewTodoKeyDown(event: ReactKeyboardEventI): Option[IO[Unit]] = /**
Some((event.nativeEvent.keyCode, UnfinishedTitle(event.target.value).validated)) collect { * A backend lives for the entire life of a component. During that time,
case (KeyCode.Enter, Some(title)) => * it might receive new Props,
IO(event.target.value = "") |+| $.props.model.addTodo(title) * so we use this mechanism to keep state that is derived from Props, so
} * we only update it again if Props changed in a meaningful way (as determined
* by the implicit `Reusability` defined above )
def updateTitle(id: TodoId)(title: Title): IO[Unit] = */
editingDone(cb = $.props.model.update(id, title)) case class Callbacks(P: Props) {
val handleNewTodoKeyDown: ReactKeyboardEventI => Option[Callback] =
e => Some((e.nativeEvent.keyCode, UnfinishedTitle(e.target.value).validated)) collect {
case (KeyCode.Enter, Some(title)) =>
Callback(e.target.value = "") >> P.model.addTodo(title)
}
val updateTitle: TodoId => Title => Callback =
id => title => editingDone(cb = P.model.update(id, title))
val toggleAll: ReactEventI => Callback =
e => P.model.toggleAll(e.target.checked)
}
def startEditing(id: TodoId): IO[Unit] = val cbs = Px.cbA($.props).map(Callbacks)
$.modStateIO(_.copy(editing = Some(id)))
val startEditing: TodoId => Callback =
id => $.modState(_.copy(editing = Some(id)))
/** /**
* @param cb Two changes to the same `State` must be combined using a callback like this. * @param cb Two changes to the same `State` must be combined using a callback like this.
* If not, rerendering will prohibit the second from having its effect. * If not, rerendering will prohibit the second from having its effect.
* For this example, the current `State` contains both `editing` and the list of todos. * For this example, the current `State` contains both `editing` and the list of todos.
*/ */
def editingDone(cb: OpCallbackIO = js.undefined): IO[Unit] = def editingDone(cb: Callback = Callback.empty): Callback =
$.modStateIO(_.copy(editing = None), cb) $.modState(_.copy(editing = None), cb)
def toggleAll(e: ReactEventI): IO[Unit] =
$.props.model.toggleAll(e.target.checked)
def render: ReactElement = { def render(P: Props, S: State): ReactTagOf[html.Div] = {
val todos = $.state.todos val todos = S.todos
val filteredTodos = todos filter $.props.currentFilter.accepts val filteredTodos = todos filter P.currentFilter.accepts
val activeCount = todos count TodoFilter.Active.accepts val activeCount = todos count TodoFilter.Active.accepts
val completedCount = todos.length - activeCount val completedCount = todos.length - activeCount
/**
* `cbs.value()` checks if `Props` changed (according to `Reusability`),
* and, if it did, creates a new instance of `Callbacks`. For best
* performance, it's best to call value() once per render() pass.
*/
val callbacks = cbs.value()
<.div( <.div(
<.h1("todos"), <.h1("todos"),
<.header( <.header(
...@@ -70,62 +89,65 @@ object CTodoList { ...@@ -70,62 +89,65 @@ object CTodoList {
<.input( <.input(
^.className := "new-todo", ^.className := "new-todo",
^.placeholder := "What needs to be done?", ^.placeholder := "What needs to be done?",
^.onKeyDown ~~>? handleNewTodoKeyDown _, ^.onKeyDown ==>? callbacks.handleNewTodoKeyDown,
^.autoFocus := true ^.autoFocus := true
) )
), ),
todos.nonEmpty ?= todoList(filteredTodos, activeCount), todos.nonEmpty ?= todoList(P, callbacks, S.editing, filteredTodos, activeCount),
todos.nonEmpty ?= footer(activeCount, completedCount) todos.nonEmpty ?= footer(P, activeCount, completedCount)
) )
} }
def todoList(filteredTodos: Seq[Todo], activeCount: Int): ReactElement = def todoList(P: Props,
callbacks: Callbacks,
editing: Option[TodoId],
filteredTodos: Seq[Todo],
activeCount: Int): ReactTagOf[html.Element] =
<.section( <.section(
^.className := "main", ^.className := "main",
<.input( <.input(
^.className := "toggle-all", ^.className := "toggle-all",
^.`type` := "checkbox", ^.`type` := "checkbox",
^.checked := activeCount == 0, ^.checked := activeCount == 0,
^.onChange ~~> toggleAll _ ^.onChange ==> callbacks.toggleAll
), ),
<.ul( <.ul(
^.className := "todo-list", ^.className := "todo-list",
filteredTodos.map(todo => filteredTodos.map(todo =>
CTodoItem( CTodoItem(CTodoItem.Props(
onToggle = $.props.model.toggleCompleted(todo.id), onToggle = P.model.toggleCompleted(todo.id),
onDelete = $.props.model.delete(todo.id), onDelete = P.model.delete(todo.id),
onStartEditing = startEditing(todo.id), onStartEditing = startEditing(todo.id),
onUpdateTitle = updateTitle(todo.id), onUpdateTitle = callbacks.updateTitle(todo.id),
onCancelEditing = editingDone(), onCancelEditing = editingDone(),
todo = todo, todo = todo,
isEditing = $.state.editing.contains(todo.id) isEditing = editing.contains(todo.id)
) ))
) )
) )
) )
def footer(activeCount: Int, completedCount: Int): ReactElement = def footer(P: Props, activeCount: Int, completedCount: Int): ReactElement =
CFooter( CFooter(CFooter.Props(
filterLink = $.props.ctl.link, filterLink = P.ctl.link,
onClearCompleted = $.props.model.clearCompleted, onClearCompleted = P.model.clearCompleted,
currentFilter = $.props.currentFilter, currentFilter = P.currentFilter,
activeCount = activeCount, activeCount = activeCount,
completedCount = completedCount completedCount = completedCount
) ))
} }
private val component = ReactComponentB[Props]("CTodoList") private val component = ReactComponentB[Props]("CTodoList")
/* state derived from the props */ /* state derived from the props */
.initialStateP(p => State(p.model.todos, None)) .initialState_P(p => State(p.model.todos, None))
.backend(Backend) .renderBackend[Backend]
.render(_.backend.render)
/** /**
* Makes the component subscribe to events coming from the model. * Makes the component subscribe to events coming from the model.
* Unsubscription on component unmount is handled automatically. * Unsubscription on component unmount is handled automatically.
* The last function is the actual event handling, in this case * The last function is the actual event handling, in this case
* we just overwrite the whole list in `state`. * we just overwrite the whole list in `state`.
*/ */
.configure(Listenable.installIO((p: Props) => p.model, ($, todos: Seq[Todo]) => $.modStateIO(_.copy(todos = todos)))) .configure(Listenable.install((p: Props) => p.model, $ => (todos: Seq[Todo]) => $.modState(_.copy(todos = todos))))
/** /**
* Optimization where we specify whether the component can have changed. * Optimization where we specify whether the component can have changed.
* In this case we avoid comparing model and routerConfig, and only do * In this case we avoid comparing model and routerConfig, and only do
......
package todomvc package todomvc
import japgolly.scalajs.react._ import japgolly.scalajs.react._
import japgolly.scalajs.react.extra.router2._ import japgolly.scalajs.react.extra.router._
import org.scalajs.dom import org.scalajs.dom
import scala.scalajs.js.JSApp import scala.scalajs.js.JSApp
import scala.scalajs.js.annotation.JSExport import scala.scalajs.js.annotation.JSExport
import scalaz.std.list._
import scalaz.syntax.foldable._
object Main extends JSApp { object Main extends JSApp {
val baseUrl = BaseUrl(dom.window.location.href.takeWhile(_ != '#')) val baseUrl = BaseUrl(dom.window.location.href.takeWhile(_ != '#'))
...@@ -18,12 +16,8 @@ object Main extends JSApp { ...@@ -18,12 +16,8 @@ object Main extends JSApp {
/* how the application renders the list given a filter */ /* how the application renders the list given a filter */
def filterRoute(s: TodoFilter): Rule = staticRoute("#/" + s.link, s) ~> renderR(CTodoList(model, s)) def filterRoute(s: TodoFilter): Rule = staticRoute("#/" + s.link, s) ~> renderR(CTodoList(model, s))
/** Combine routes for all filters using the Monoid instance for `Rule`. val filterRoutes: Rule = TodoFilter.values.map(filterRoute).reduce(_ | _)
* This could have been written out as
* `filterRoute(TodoFilter.All) | filterRoute(TodoFilter.Active) | filterRoute(TodoFilter.Completed)`
*/
val filterRoutes: Rule = (TodoFilter.values map filterRoute).suml
/* build a final RouterConfig with a default page */ /* build a final RouterConfig with a default page */
filterRoutes.notFound(redirectToPage(TodoFilter.All)(Redirect.Replace)) filterRoutes.notFound(redirectToPage(TodoFilter.All)(Redirect.Replace))
} }
...@@ -31,7 +25,7 @@ object Main extends JSApp { ...@@ -31,7 +25,7 @@ object Main extends JSApp {
/* instantiate model and restore todos */ /* instantiate model and restore todos */
val model = new TodoModel(Storage(dom.ext.LocalStorage, "todos-scalajs-react")) val model = new TodoModel(Storage(dom.ext.LocalStorage, "todos-scalajs-react"))
model.restorePersisted.foreach(_.unsafePerformIO()) model.restorePersisted.foreach(_.runNow())
/** The router is itself a React component, which at this point is not mounted (U-suffix) */ /** The router is itself a React component, which at this point is not mounted (U-suffix) */
val router: ReactComponentU[Unit, Resolution[TodoFilter], Any, TopNode] = val router: ReactComponentU[Unit, Resolution[TodoFilter], Any, TopNode] =
...@@ -44,6 +38,7 @@ object Main extends JSApp { ...@@ -44,6 +38,7 @@ object Main extends JSApp {
* will render into the first element with `todoapp` class * will render into the first element with `todoapp` class
*/ */
@JSExport @JSExport
override def main() = override def main(): Unit = {
React.render(router, dom.document.getElementsByClassName("todoapp")(0)) val mounted = ReactDOM.render(router, dom.document.getElementsByClassName("todoapp")(0))
}
} }
package todomvc package todomvc
import japgolly.scalajs.react.Callback
import org.scalajs.dom import org.scalajs.dom
import upickle.default._ import upickle.default._
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
case class Storage(storage: dom.ext.Storage, namespace: String) { case class Storage(storage: dom.ext.Storage, namespace: String) {
def store[T: Writer](data: T) = def store[T: Writer](data: T): Callback =
storage(namespace) = write(data) Callback(storage(namespace) = write(data))
def load[T: Reader]: Option[T] = def load[T: Reader]: Option[T] =
Try(storage(namespace) map read[T]) match { Try(storage(namespace) map read[T]) match {
......
package todomvc package todomvc
import japgolly.scalajs.react.Callback
import japgolly.scalajs.react.extra.Broadcaster import japgolly.scalajs.react.extra.Broadcaster
import scala.language.postfixOps import scala.language.postfixOps
import scalaz.effect.IO
class TodoModel(storage: Storage) extends Broadcaster[Seq[Todo]] { class TodoModel(storage: Storage) extends Broadcaster[Seq[Todo]] {
private object State { private object State {
var todos = Seq.empty[Todo] var todos = Seq.empty[Todo]
def mod(f: Seq[Todo] => Seq[Todo]): IO[Unit] = def mod(f: Seq[Todo] => Seq[Todo]): Callback = {
IO { val newTodos = f(todos)
val newTodos = f(todos)
todos = newTodos
storage.store(newTodos)
broadcast(newTodos)
}
def modOne(Id: TodoId)(f: Todo => Todo): IO[Unit] = Callback(todos = newTodos) >>
storage.store(newTodos) >>
broadcast(newTodos)
}
def modOne(Id: TodoId)(f: Todo => Todo): Callback =
mod(_.map { mod(_.map {
case existing@Todo(Id, _, _) => f(existing) case existing@Todo(Id, _, _) => f(existing)
case other => other case other => other
...@@ -27,24 +27,24 @@ class TodoModel(storage: Storage) extends Broadcaster[Seq[Todo]] { ...@@ -27,24 +27,24 @@ class TodoModel(storage: Storage) extends Broadcaster[Seq[Todo]] {
def restorePersisted = def restorePersisted =
storage.load[Seq[Todo]].map(existing => State.mod(_ ++ existing)) storage.load[Seq[Todo]].map(existing => State.mod(_ ++ existing))
def addTodo(title: Title): IO[Unit] = def addTodo(title: Title): Callback =
State.mod(_ :+ Todo(TodoId.random, title, isCompleted = false)) State.mod(_ :+ Todo(TodoId.random, title, isCompleted = false))
def clearCompleted: IO[Unit] = def clearCompleted: Callback =
State.mod(_.filterNot(_.isCompleted)) State.mod(_.filterNot(_.isCompleted))
def delete(id: TodoId): IO[Unit] = def delete(id: TodoId): Callback =
State.mod(_.filterNot(_.id == id)) State.mod(_.filterNot(_.id == id))
def todos: Seq[Todo] = def todos: Seq[Todo] =
State.todos State.todos
def toggleAll(checked: Boolean): IO[Unit] = def toggleAll(checked: Boolean): Callback =
State.mod(_.map(_.copy(isCompleted = checked))) State.mod(_.map(_.copy(isCompleted = checked)))
def toggleCompleted(id: TodoId): IO[Unit] = def toggleCompleted(id: TodoId): Callback =
State.modOne(id)(old => old.copy(isCompleted = !old.isCompleted)) State.modOne(id)(old => old.copy(isCompleted = !old.isCompleted))
def update(id: TodoId, text: Title): IO[Unit] = def update(id: TodoId, text: Title): Callback =
State.modOne(id)(_.copy(title = text)) State.modOne(id)(_.copy(title = text))
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment