Commit 40bada0c authored by Øyvind Raddum Berg's avatar Øyvind Raddum Berg Committed by Sam Saccone

Scalajs-React updates (#1656)

* Scalajs-React: Reformat, drop C-prefix

* Scalajs-React: Convert to .sbt file, bump dependencies

* Scalajs-React: Capture event data and correctly focus input field

* Scalajs-React: Generate new files
parent 083b718c
enablePlugins(ScalaJSPlugin)
name := "todomvc"
organization := "com.olvind"
version := "1"
licenses += ("Apache-2.0", url("http://opensource.org/licenses/Apache-2.0"))
scalaVersion := "2.11.8"
scalacOptions ++= Seq("-deprecation", "-encoding", "UTF-8", "-feature", "-unchecked", "-Xlint", "-Yno-adapted-args", "-Ywarn-dead-code", "-Ywarn-value-discard" )
/* create javascript launcher. Searches for an object extends JSApp */
persistLauncher := true
/* javascript dependencies */
jsDependencies ++= Seq(
"org.webjars.bower" % "react" % "15.2.1" / "react-with-addons.js" minified "react-with-addons.min.js" commonJSName "React",
"org.webjars.bower" % "react" % "15.2.1" / "react-dom.js" minified "react-dom.min.js" dependsOn "react-with-addons.js" commonJSName "ReactDOM"
)
/* scala.js dependencies */
libraryDependencies ++= Seq(
"com.github.japgolly.scalajs-react" %%% "extra" % "0.11.1",
"com.lihaoyi" %%% "upickle" % "0.4.1"
)
/* move these files out of target/. Also sets up same file for both fast and full optimization */
val generatedDir = file("generated")
crossTarget in (Compile, fullOptJS) := generatedDir
crossTarget in (Compile, fastOptJS) := generatedDir
crossTarget in (Compile, packageJSDependencies) := generatedDir
crossTarget in (Compile, packageScalaJSLauncher) := generatedDir
crossTarget in (Compile, packageMinifiedJSDependencies) := generatedDir
artifactPath in (Compile, fastOptJS) :=
((crossTarget in (Compile, fastOptJS)).value / ((moduleName in fastOptJS).value + "-opt.js"))
\ No newline at end of file
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.
import sbt._
import Keys._
import org.scalajs.sbtplugin.ScalaJSPlugin
import ScalaJSPlugin.autoImport._
object Build extends Build {
val scalaJsReactVersion = "0.10.0"
val generatedDir = file("generated")
val todomvc = Project("todomvc", file("."))
.enablePlugins(ScalaJSPlugin)
.settings(
organization := "com.olvind",
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", "-Ywarn-value-discard" ),
updateOptions := updateOptions.value.withCachedResolution(true),
sbt.Keys.test in Test := (),
emitSourceMaps := true,
/* create javascript launcher. Searches for an object extends JSApp */
persistLauncher := true,
/* javascript dependencies */
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" %%% "extra" % scalaJsReactVersion,
"com.lihaoyi" %%% "upickle" % "0.3.5"
),
/* move these files out of target/. Also sets up same file for both fast and full optimization */
crossTarget in (Compile, fullOptJS) := generatedDir,
crossTarget in (Compile, fastOptJS) := generatedDir,
crossTarget in (Compile, packageJSDependencies) := generatedDir,
crossTarget in (Compile, packageScalaJSLauncher) := generatedDir,
crossTarget in (Compile, packageMinifiedJSDependencies) := generatedDir,
artifactPath in (Compile, fastOptJS) :=
((crossTarget in (Compile, fastOptJS)).value / ((moduleName in fastOptJS).value + "-opt.js"))
)
}
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.11")
...@@ -4,7 +4,7 @@ import japgolly.scalajs.react._ ...@@ -4,7 +4,7 @@ import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._ import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom.html import org.scalajs.dom.html
object CFooter { object Footer {
case class Props( case class Props(
filterLink: TodoFilter => ReactTag, filterLink: TodoFilter => ReactTag,
...@@ -24,7 +24,12 @@ object CFooter { ...@@ -24,7 +24,12 @@ object CFooter {
) )
def filterLink(P: Props)(s: TodoFilter): ReactTagOf[html.LI] = def filterLink(P: Props)(s: TodoFilter): ReactTagOf[html.LI] =
<.li(P.filterLink(s)((P.currentFilter == s) ?= (^.className := "selected"), s.title)) <.li(
P.filterLink(s)(
s.title,
(P.currentFilter == s) ?= (^.className := "selected")
)
)
def render(P: Props): ReactTagOf[html.Element] = def render(P: Props): ReactTagOf[html.Element] =
<.footer( <.footer(
...@@ -42,10 +47,12 @@ object CFooter { ...@@ -42,10 +47,12 @@ object CFooter {
) )
} }
private val component = ReactComponentB[Props]("CFooter") private val component =
.stateless ReactComponentB[Props]("Footer")
.renderBackend[Backend] .stateless
.build .renderBackend[Backend]
.build
def apply(P: Props) = component(P) def apply(P: Props): ReactElement =
component(P)
} }
...@@ -8,22 +8,27 @@ import scala.scalajs.js.JSApp ...@@ -8,22 +8,27 @@ import scala.scalajs.js.JSApp
import scala.scalajs.js.annotation.JSExport import scala.scalajs.js.annotation.JSExport
object Main extends JSApp { object Main extends JSApp {
val baseUrl = BaseUrl(dom.window.location.href.takeWhile(_ != '#')) val baseUrl: BaseUrl =
BaseUrl(dom.window.location.href.takeWhile(_ != '#'))
val routerConfig: RouterConfig[TodoFilter] = RouterConfigDsl[TodoFilter].buildConfig { dsl => val routerConfig: RouterConfig[TodoFilter] =
import dsl._ RouterConfigDsl[TodoFilter].buildConfig { dsl =>
import dsl._
/* 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(TodoList(model, s))
val filterRoutes: Rule = TodoFilter.values.map(filterRoute).reduce(_ | _) val filterRoutes: Rule =
TodoFilter.values.map(filterRoute).reduce(_ | _)
/* 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))
} }
/* instantiate model and restore todos */ /* instantiate model and restore todos */
val model = new TodoModel(Storage(dom.ext.LocalStorage, "todos-scalajs-react")) val model: TodoModel =
new TodoModel(Storage(dom.ext.LocalStorage, "todos-scalajs-react"))
model.restorePersisted.foreach(_.runNow()) model.restorePersisted.foreach(_.runNow())
......
package todomvc package todomvc
import japgolly.scalajs.react._ import japgolly.scalajs.react._
import japgolly.scalajs.react.extra.{Reusability, Px} import japgolly.scalajs.react.extra.{Px, Reusability}
import japgolly.scalajs.react.vdom.prefix_<^._ import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom
import org.scalajs.dom.ext.KeyCode import org.scalajs.dom.ext.KeyCode
object CTodoItem { object TodoItem {
case class Props ( case class Props (
onToggle: Callback, onToggle: Callback,
...@@ -17,13 +18,17 @@ object CTodoItem { ...@@ -17,13 +18,17 @@ object CTodoItem {
isEditing: Boolean isEditing: Boolean
) )
implicit val reusableProps = Reusability.fn[Props]((p1, p2) => implicit val reusableProps: Reusability[Props] =
(p1.todo eq p2.todo) && (p1.isEditing == p2.isEditing) Reusability.fn[Props]((p1, p2) =>
) (p1.todo eq p2.todo) && (p1.isEditing == p2.isEditing)
)
case class State(editText: UnfinishedTitle) case class State(editText: UnfinishedTitle)
class Backend($: BackendScope[Props, State]) { class Backend($: BackendScope[Props, State]) {
val inputRef: RefSimple[dom.html.Input] =
Ref.apply[dom.html.Input]("input")
case class Callbacks(P: Props) { case class Callbacks(P: Props) {
val editFieldSubmit: Callback = val editFieldSubmit: Callback =
$.state.flatMap(_.editText.validated.fold(P.onDelete)(P.onUpdateTitle)) $.state.flatMap(_.editText.validated.fold(P.onDelete)(P.onUpdateTitle))
...@@ -38,10 +43,16 @@ object CTodoItem { ...@@ -38,10 +43,16 @@ object CTodoItem {
case _ => None case _ => None
} }
} }
val cbs = Px.cbA($.props).map(Callbacks)
val cbs: Px[Callbacks] =
Px.cbA($.props).map(Callbacks)
val editFieldChanged: ReactEventI => Callback = val editFieldChanged: ReactEventI => Callback =
e => $.modState(_.copy(editText = UnfinishedTitle(e.target.value))) e => {
/* need to capture event data because React reuses events */
val captured = e.target.value
$.modState(_.copy(editText = UnfinishedTitle(captured)))
}
def render(P: Props, S: State): ReactElement = { def render(P: Props, S: State): ReactElement = {
val cb = cbs.value() val cb = cbs.value()
...@@ -68,6 +79,7 @@ object CTodoItem { ...@@ -68,6 +79,7 @@ object CTodoItem {
) )
), ),
<.input( <.input(
^.ref := inputRef,
^.className := "edit", ^.className := "edit",
^.onBlur --> cb.editFieldSubmit, ^.onBlur --> cb.editFieldSubmit,
^.onChange ==> editFieldChanged, ^.onChange ==> editFieldChanged,
...@@ -78,10 +90,19 @@ object CTodoItem { ...@@ -78,10 +90,19 @@ object CTodoItem {
} }
} }
val component = ReactComponentB[Props]("CTodoItem") private val component =
.initialState_P(p => State(p.todo.title.editable)) ReactComponentB[Props]("TodoItem")
.renderBackend[Backend].build .initialState_P(p => State(p.todo.title.editable))
.renderBackend[Backend]
.componentDidUpdate {
case ComponentDidUpdate(c, prevProps, _)
c.backend.inputRef(c)
.tryFocus
.when(c.props.isEditing && !prevProps.isEditing)
.void
}
.build
def apply(P: Props) = def apply(P: Props): ReactElement =
component.withKey(P.todo.id.id.toString)(P) component.withKey(P.todo.id.id.toString)(P)
} }
...@@ -7,9 +7,9 @@ import japgolly.scalajs.react.vdom.prefix_<^._ ...@@ -7,9 +7,9 @@ import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom.ext.KeyCode import org.scalajs.dom.ext.KeyCode
import org.scalajs.dom.html import org.scalajs.dom.html
object CTodoList { object TodoList {
case class Props private[CTodoList] ( case class Props (
ctl: RouterCtl[TodoFilter], ctl: RouterCtl[TodoFilter],
model: TodoModel, model: TodoModel,
currentFilter: TodoFilter currentFilter: TodoFilter
...@@ -23,8 +23,10 @@ object CTodoList { ...@@ -23,8 +23,10 @@ object CTodoList {
/** /**
* 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)
*/ */
implicit val r1 = Reusability.fn[Props]((p1, p2) => p1.currentFilter == p2.currentFilter) implicit val r1: Reusability[Props] =
implicit val r2 = Reusability.fn[State]((s1, s2) => s1.editing == s2.editing && (s1.todos eq s2.todos)) Reusability.fn[Props]((p1, p2) => p1.currentFilter == p2.currentFilter)
implicit val r2: Reusability[State] =
Reusability.fn[State]((s1, s2) => s1.editing == s2.editing && (s1.todos eq s2.todos))
/** /**
* One difference between normal react and scalajs-react is the use of backends. * One difference between normal react and scalajs-react is the use of backends.
...@@ -56,7 +58,8 @@ object CTodoList { ...@@ -56,7 +58,8 @@ object CTodoList {
e => P.model.toggleAll(e.target.checked) e => P.model.toggleAll(e.target.checked)
} }
val cbs = Px.cbA($.props).map(Callbacks) val cbs: Px[Callbacks] =
Px.cbA($.props).map(Callbacks)
val startEditing: TodoId => Callback = val startEditing: TodoId => Callback =
id => $.modState(_.copy(editing = Some(id))) id => $.modState(_.copy(editing = Some(id)))
...@@ -81,7 +84,7 @@ object CTodoList { ...@@ -81,7 +84,7 @@ object CTodoList {
* and, if it did, creates a new instance of `Callbacks`. For best * and, if it did, creates a new instance of `Callbacks`. For best
* performance, it's best to call value() once per render() pass. * performance, it's best to call value() once per render() pass.
*/ */
val callbacks = cbs.value() val callbacks = cbs.value()
<.div( <.div(
<.h1("todos"), <.h1("todos"),
<.header( <.header(
...@@ -114,7 +117,7 @@ object CTodoList { ...@@ -114,7 +117,7 @@ object CTodoList {
<.ul( <.ul(
^.className := "todo-list", ^.className := "todo-list",
filteredTodos.map(todo => filteredTodos.map(todo =>
CTodoItem(CTodoItem.Props( TodoItem(TodoItem.Props(
onToggle = P.model.toggleCompleted(todo.id), onToggle = P.model.toggleCompleted(todo.id),
onDelete = P.model.delete(todo.id), onDelete = P.model.delete(todo.id),
onStartEditing = startEditing(todo.id), onStartEditing = startEditing(todo.id),
...@@ -128,7 +131,7 @@ object CTodoList { ...@@ -128,7 +131,7 @@ object CTodoList {
) )
def footer(P: Props, activeCount: Int, completedCount: Int): ReactElement = def footer(P: Props, activeCount: Int, completedCount: Int): ReactElement =
CFooter(CFooter.Props( Footer(Footer.Props(
filterLink = P.ctl.link, filterLink = P.ctl.link,
onClearCompleted = P.model.clearCompleted, onClearCompleted = P.model.clearCompleted,
currentFilter = P.currentFilter, currentFilter = P.currentFilter,
...@@ -137,34 +140,35 @@ object CTodoList { ...@@ -137,34 +140,35 @@ object CTodoList {
)) ))
} }
private val component = ReactComponentB[Props]("CTodoList") private val component =
/* state derived from the props */ ReactComponentB[Props]("TodoList")
.initialState_P(p => State(p.model.todos, None)) /* state derived from the props */
.renderBackend[Backend] .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. * Makes the component subscribe to events coming from the model.
* The last function is the actual event handling, in this case * Unsubscription on component unmount is handled automatically.
* we just overwrite the whole list in `state`. * The last function is the actual event handling, in this case
*/ * we just overwrite the whole list in `state`.
.configure(Listenable.install((p: Props) => p.model, $ => (todos: Seq[Todo]) => $.modState(_.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 * Optimization where we specify whether the component can have changed.
* reference checking on the list of todos. * In this case we avoid comparing model and routerConfig, and only do
* * reference checking on the list of todos.
* The implementation of the «equality» checks are in the Reusability *
* typeclass instances for `State` and `Props` at the top of the file. * The implementation of the «equality» checks are in the Reusability
* * typeclass instances for `State` and `Props` at the top of the file.
* To understand how things are redrawn, change `shouldComponentUpdate` for *
* either `shouldComponentUpdateWithOverlay` or `shouldComponentUpdateAndLog` * To understand how things are redrawn, change `shouldComponentUpdate` for
*/ * either `shouldComponentUpdateWithOverlay` or `shouldComponentUpdateAndLog`
.configure(Reusability.shouldComponentUpdate) */
/** .configure(Reusability.shouldComponentUpdate)
* For performance reasons its important to only call `build` once for each component /**
*/ * For performance reasons its important to only call `build` once for each component
.build */
.build
def apply(model: TodoModel, currentFilter: TodoFilter)(ctl: RouterCtl[TodoFilter]) = def apply(model: TodoModel, currentFilter: TodoFilter)(ctl: RouterCtl[TodoFilter]): ReactElement =
component(Props(ctl, model, currentFilter)) component(Props(ctl, model, currentFilter))
} }
...@@ -24,7 +24,7 @@ class TodoModel(storage: Storage) extends Broadcaster[Seq[Todo]] { ...@@ -24,7 +24,7 @@ class TodoModel(storage: Storage) extends Broadcaster[Seq[Todo]] {
}) })
} }
def restorePersisted = def restorePersisted: Option[Callback] =
storage.load[Seq[Todo]].map(existing => State.mod(_ ++ existing)) storage.load[Seq[Todo]].map(existing => State.mod(_ ++ existing))
def addTodo(title: Title): Callback = def addTodo(title: Title): Callback =
......
...@@ -25,5 +25,6 @@ object TodoFilter { ...@@ -25,5 +25,6 @@ object TodoFilter {
object Active extends TodoFilter("active", "Active", !_.isCompleted) object Active extends TodoFilter("active", "Active", !_.isCompleted)
object Completed extends TodoFilter("completed", "Completed", _.isCompleted) object Completed extends TodoFilter("completed", "Completed", _.isCompleted)
def values = List[TodoFilter](All, Active, Completed) def values: List[TodoFilter] =
List(All, Active, Completed)
} }
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