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.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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