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
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