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
import ScalaJSPlugin.autoImport._
object Build extends Build {
val scalaJsReactVersion = "0.9.1"
val scalaJsReactVersion = "0.10.0"
val generatedDir = file("generated")
val todomvc = Project("todomvc", file("."))
......@@ -15,7 +15,7 @@ object Build extends Build {
version := "1",
licenses += ("Apache-2.0", url("http://opensource.org/licenses/Apache-2.0")),
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),
sbt.Keys.test in Test := (),
emitSourceMaps := true,
......@@ -23,14 +23,15 @@ object Build extends Build {
persistLauncher := true,
/* javascript dependencies */
jsDependencies += "org.webjars" % "react" % "0.12.1" /
"react-with-addons.js" commonJSName "React" minified "react-with-addons.min.js",
jsDependencies ++= Seq(
"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 */
libraryDependencies ++= Seq(
"com.github.japgolly.scalajs-react" %%% "ext-scalaz71" % 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 */
......
package todomvc
import japgolly.scalajs.react.ScalazReact._
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._
import scalaz.effect.IO
import scalaz.syntax.std.list._
import scalaz.syntax.std.string._
import org.scalajs.dom.html
object CFooter {
case class Props private[CFooter](
case class Props(
filterLink: TodoFilter => ReactTag,
onClearCompleted: IO[Unit],
onClearCompleted: Callback,
currentFilter: TodoFilter,
activeCount: Int,
completedCount: Int
)
case class Backend($: BackendScope[Props, Unit]) {
val clearButton =
<.button(^.className := "clear-completed", ^.onClick ~~> $.props.onClearCompleted, "Clear completed")
def filterLink(s: TodoFilter) =
<.li($.props.filterLink(s)(($.props.currentFilter == s) ?= (^.className := "selected"), s.title))
class Backend($: BackendScope[Props, Unit]) {
def clearButton(P: Props): ReactTagOf[html.Button] =
<.button(
^.className := "clear-completed",
^.onClick --> P.onClearCompleted,
"Clear completed",
(P.completedCount == 0) ?= ^.visibility.hidden
)
def withSpaces(ts: TagMod*) =
ts.toList.intersperse(" ")
def filterLink(P: Props)(s: TodoFilter): ReactTagOf[html.LI] =
<.li(P.filterLink(s)((P.currentFilter == s) ?= (^.className := "selected"), s.title))
def render =
def render(P: Props): ReactTagOf[html.Element] =
<.footer(
^.className := "footer",
<.span(
^.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(
^.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")
.stateless
.backend(Backend)
.render(_.backend.render)
.renderBackend[Backend]
.build
def apply(filterLink: TodoFilter => ReactTag,
onClearCompleted:IO[Unit],
currentFilter: TodoFilter,
activeCount: Int,
completedCount: Int) =
component(
Props(
filterLink = filterLink,
onClearCompleted = onClearCompleted,
currentFilter = currentFilter,
activeCount = activeCount,
completedCount = completedCount
)
)
def apply(P: Props) = component(P)
}
package todomvc
import japgolly.scalajs.react.ScalazReact._
import japgolly.scalajs.react._
import japgolly.scalajs.react.extra.{Reusability, Px}
import japgolly.scalajs.react.vdom.prefix_<^._
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 {
case class Props private[CTodoItem] (
onToggle: IO[Unit],
onDelete: IO[Unit],
onStartEditing: IO[Unit],
onUpdateTitle: Title => IO[Unit],
onCancelEditing: IO[Unit],
case class Props (
onToggle: Callback,
onDelete: Callback,
onStartEditing: Callback,
onUpdateTitle: Title => Callback,
onCancelEditing: Callback,
todo: Todo,
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] =
$.state.editText.validated.fold($.props.onDelete)($.props.onUpdateTitle)
class Backend($: BackendScope[Props, State]) {
case class Callbacks(P: Props) {
val editFieldSubmit: Callback =
$.state.flatMap(_.editText.validated.fold(P.onDelete)(P.onUpdateTitle))
/**
* It's OK to make these into `val`s as long as they don't touch state.
*/
val resetText: IO[Unit] =
$.modStateIO(_.copy(editText = $.props.todo.title.editable))
val resetText: Callback =
$.modState(_.copy(editText = P.todo.title.editable))
val editFieldKeyDown: ReactKeyboardEvent => Option[IO[Unit]] =
val editFieldKeyDown: ReactKeyboardEvent => Option[Callback] =
e => e.nativeEvent.keyCode match {
case KeyCode.Escape => (resetText |+| $.props.onCancelEditing).some
case KeyCode.Enter => editFieldSubmit.some
case KeyCode.Escape => Some(resetText >> P.onCancelEditing)
case KeyCode.Enter => Some(editFieldSubmit)
case _ => None
}
}
val cbs = Px.cbA($.props).map(Callbacks)
val editFieldChanged: ReactEventI => IO[Unit] =
e => $.modStateIO(_.copy(editText = UnfinishedTitle(e.target.value)))
val editFieldChanged: ReactEventI => Callback =
e => $.modState(_.copy(editText = UnfinishedTitle(e.target.value)))
def render: ReactElement = {
def render(P: Props, S: State): ReactElement = {
val cb = cbs.value()
<.li(
^.classSet(
"completed" -> $.props.todo.isCompleted,
"editing" -> $.props.isEditing
"completed" -> P.todo.isCompleted,
"editing" -> P.isEditing
),
<.div(
^.className := "view",
<.input(
^.className := "toggle",
^.`type` := "checkbox",
^.checked := $.props.todo.isCompleted,
^.onChange ~~> $.props.onToggle
^.checked := P.todo.isCompleted,
^.onChange --> P.onToggle
),
<.label(
P.todo.title.value,
^.onDoubleClick --> P.onStartEditing
),
<.label($.props.todo.title.value, ^.onDoubleClick ~~> $.props.onStartEditing),
<.button(^.className := "destroy", ^.onClick ~~> $.props.onDelete)
<.button(
^.className := "destroy",
^.onClick --> P.onDelete
)
),
<.input(
^.className := "edit",
^.onBlur ~~> editFieldSubmit,
^.onChange ~~> editFieldChanged,
^.onKeyDown ~~>? editFieldKeyDown,
^.value := $.state.editText.value
^.onBlur --> cb.editFieldSubmit,
^.onChange ==> editFieldChanged,
^.onKeyDown ==>? cb.editFieldKeyDown,
^.value := S.editText.value
)
)
}
}
private val component = ReactComponentB[Props]("CTodoItem")
.initialStateP(p => State(p.todo.title.editable))
.backend(Backend)
.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) =
val component = ReactComponentB[Props]("CTodoItem")
.initialState_P(p => State(p.todo.title.editable))
.renderBackend[Backend].build
component.withKey(todo.id.id.toString)(
Props(
onToggle = onToggle,
onDelete = onDelete,
onStartEditing = onStartEditing,
onUpdateTitle = onUpdateTitle,
onCancelEditing = onCancelEditing,
todo = todo,
isEditing = isEditing
)
)
def apply(P: Props) =
component.withKey(P.todo.id.id.toString)(P)
}
package todomvc
import japgolly.scalajs.react.ScalazReact._
import japgolly.scalajs.react._
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 org.scalajs.dom.ext.KeyCode
import scala.scalajs.js
import scalaz.effect.IO
import scalaz.std.anyVal.unitInstance
import scalaz.syntax.semigroup._
import org.scalajs.dom.html
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)
......@@ -31,38 +33,55 @@ object CTodoList {
*
* 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,
* it might receive new Props,
* 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 )
*/
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)) =>
IO(event.target.value = "") |+| $.props.model.addTodo(title)
Callback(e.target.value = "") >> P.model.addTodo(title)
}
def updateTitle(id: TodoId)(title: Title): IO[Unit] =
editingDone(cb = $.props.model.update(id, 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] =
$.modStateIO(_.copy(editing = Some(id)))
val cbs = Px.cbA($.props).map(Callbacks)
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.
* 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.
*/
def editingDone(cb: OpCallbackIO = js.undefined): IO[Unit] =
$.modStateIO(_.copy(editing = None), cb)
def toggleAll(e: ReactEventI): IO[Unit] =
$.props.model.toggleAll(e.target.checked)
def editingDone(cb: Callback = Callback.empty): Callback =
$.modState(_.copy(editing = None), cb)
def render: ReactElement = {
val todos = $.state.todos
val filteredTodos = todos filter $.props.currentFilter.accepts
def render(P: Props, S: State): ReactTagOf[html.Div] = {
val todos = S.todos
val filteredTodos = todos filter P.currentFilter.accepts
val activeCount = todos count TodoFilter.Active.accepts
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(
<.h1("todos"),
<.header(
......@@ -70,62 +89,65 @@ object CTodoList {
<.input(
^.className := "new-todo",
^.placeholder := "What needs to be done?",
^.onKeyDown ~~>? handleNewTodoKeyDown _,
^.onKeyDown ==>? callbacks.handleNewTodoKeyDown,
^.autoFocus := true
)
),
todos.nonEmpty ?= todoList(filteredTodos, activeCount),
todos.nonEmpty ?= footer(activeCount, completedCount)
todos.nonEmpty ?= todoList(P, callbacks, S.editing, filteredTodos, activeCount),
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(
^.className := "main",
<.input(
^.className := "toggle-all",
^.`type` := "checkbox",
^.checked := activeCount == 0,
^.onChange ~~> toggleAll _
^.onChange ==> callbacks.toggleAll
),
<.ul(
^.className := "todo-list",
filteredTodos.map(todo =>
CTodoItem(
onToggle = $.props.model.toggleCompleted(todo.id),
onDelete = $.props.model.delete(todo.id),
CTodoItem(CTodoItem.Props(
onToggle = P.model.toggleCompleted(todo.id),
onDelete = P.model.delete(todo.id),
onStartEditing = startEditing(todo.id),
onUpdateTitle = updateTitle(todo.id),
onUpdateTitle = callbacks.updateTitle(todo.id),
onCancelEditing = editingDone(),
todo = todo,
isEditing = $.state.editing.contains(todo.id)
)
isEditing = editing.contains(todo.id)
))
)
)
)
def footer(activeCount: Int, completedCount: Int): ReactElement =
CFooter(
filterLink = $.props.ctl.link,
onClearCompleted = $.props.model.clearCompleted,
currentFilter = $.props.currentFilter,
def footer(P: Props, activeCount: Int, completedCount: Int): ReactElement =
CFooter(CFooter.Props(
filterLink = P.ctl.link,
onClearCompleted = P.model.clearCompleted,
currentFilter = P.currentFilter,
activeCount = activeCount,
completedCount = completedCount
)
))
}
private val component = ReactComponentB[Props]("CTodoList")
/* state derived from the props */
.initialStateP(p => State(p.model.todos, None))
.backend(Backend)
.render(_.backend.render)
.initialState_P(p => State(p.model.todos, None))
.renderBackend[Backend]
/**
* Makes the component subscribe to events coming from the model.
* Unsubscription on component unmount is handled automatically.
* The last function is the actual event handling, in this case
* 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.
* In this case we avoid comparing model and routerConfig, and only do
......
package todomvc
import japgolly.scalajs.react._
import japgolly.scalajs.react.extra.router2._
import japgolly.scalajs.react.extra.router._
import org.scalajs.dom
import scala.scalajs.js.JSApp
import scala.scalajs.js.annotation.JSExport
import scalaz.std.list._
import scalaz.syntax.foldable._
object Main extends JSApp {
val baseUrl = BaseUrl(dom.window.location.href.takeWhile(_ != '#'))
......@@ -18,11 +16,7 @@ object Main extends JSApp {
/* how the application renders the list given a filter */
def filterRoute(s: TodoFilter): Rule = staticRoute("#/" + s.link, s) ~> renderR(CTodoList(model, s))
/** Combine routes for all filters using the Monoid instance for `Rule`.
* This could have been written out as
* `filterRoute(TodoFilter.All) | filterRoute(TodoFilter.Active) | filterRoute(TodoFilter.Completed)`
*/
val filterRoutes: Rule = (TodoFilter.values map filterRoute).suml
val filterRoutes: Rule = TodoFilter.values.map(filterRoute).reduce(_ | _)
/* build a final RouterConfig with a default page */
filterRoutes.notFound(redirectToPage(TodoFilter.All)(Redirect.Replace))
......@@ -31,7 +25,7 @@ object Main extends JSApp {
/* instantiate model and restore todos */
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) */
val router: ReactComponentU[Unit, Resolution[TodoFilter], Any, TopNode] =
......@@ -44,6 +38,7 @@ object Main extends JSApp {
* will render into the first element with `todoapp` class
*/
@JSExport
override def main() =
React.render(router, dom.document.getElementsByClassName("todoapp")(0))
override def main(): Unit = {
val mounted = ReactDOM.render(router, dom.document.getElementsByClassName("todoapp")(0))
}
}
package todomvc
import japgolly.scalajs.react.Callback
import org.scalajs.dom
import upickle.default._
import scala.util.{Failure, Success, Try}
case class Storage(storage: dom.ext.Storage, namespace: String) {
def store[T: Writer](data: T) =
storage(namespace) = write(data)
def store[T: Writer](data: T): Callback =
Callback(storage(namespace) = write(data))
def load[T: Reader]: Option[T] =
Try(storage(namespace) map read[T]) match {
......
package todomvc
import japgolly.scalajs.react.Callback
import japgolly.scalajs.react.extra.Broadcaster
import scala.language.postfixOps
import scalaz.effect.IO
class TodoModel(storage: Storage) extends Broadcaster[Seq[Todo]] {
private object State {
var todos = Seq.empty[Todo]
def mod(f: Seq[Todo] => Seq[Todo]): IO[Unit] =
IO {
def mod(f: Seq[Todo] => Seq[Todo]): Callback = {
val newTodos = f(todos)
todos = newTodos
storage.store(newTodos)
Callback(todos = newTodos) >>
storage.store(newTodos) >>
broadcast(newTodos)
}
def modOne(Id: TodoId)(f: Todo => Todo): IO[Unit] =
def modOne(Id: TodoId)(f: Todo => Todo): Callback =
mod(_.map {
case existing@Todo(Id, _, _) => f(existing)
case other => other
......@@ -27,24 +27,24 @@ class TodoModel(storage: Storage) extends Broadcaster[Seq[Todo]] {
def restorePersisted =
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))
def clearCompleted: IO[Unit] =
def clearCompleted: Callback =
State.mod(_.filterNot(_.isCompleted))
def delete(id: TodoId): IO[Unit] =
def delete(id: TodoId): Callback =
State.mod(_.filterNot(_.id == id))
def todos: Seq[Todo] =
State.todos
def toggleAll(checked: Boolean): IO[Unit] =
def toggleAll(checked: Boolean): Callback =
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))
def update(id: TodoId, text: Title): IO[Unit] =
def update(id: TodoId, text: Title): Callback =
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