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.
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._
import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom.html
object CFooter {
object Footer {
case class Props(
filterLink: TodoFilter => ReactTag,
......@@ -24,7 +24,12 @@ object CFooter {
)
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] =
<.footer(
......@@ -42,10 +47,12 @@ object CFooter {
)
}
private val component = ReactComponentB[Props]("CFooter")
.stateless
.renderBackend[Backend]
.build
private val component =
ReactComponentB[Props]("Footer")
.stateless
.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
import scala.scalajs.js.annotation.JSExport
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 =>
import dsl._
val routerConfig: RouterConfig[TodoFilter] =
RouterConfigDsl[TodoFilter].buildConfig { dsl =>
import dsl._
/* how the application renders the list given a filter */
def filterRoute(s: TodoFilter): Rule = staticRoute("#/" + s.link, s) ~> renderR(CTodoList(model, s))
/* how the application renders the list given a filter */
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 */
filterRoutes.notFound(redirectToPage(TodoFilter.All)(Redirect.Replace))
}
/* build a final RouterConfig with a default page */
filterRoutes.notFound(redirectToPage(TodoFilter.All)(Redirect.Replace))
}
/* 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())
......
package todomvc
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 org.scalajs.dom
import org.scalajs.dom.ext.KeyCode
object CTodoItem {
object TodoItem {
case class Props (
onToggle: Callback,
......@@ -17,13 +18,17 @@ object CTodoItem {
isEditing: Boolean
)
implicit val reusableProps = Reusability.fn[Props]((p1, p2) =>
(p1.todo eq p2.todo) && (p1.isEditing == p2.isEditing)
)
implicit val reusableProps: Reusability[Props] =
Reusability.fn[Props]((p1, p2) =>
(p1.todo eq p2.todo) && (p1.isEditing == p2.isEditing)
)
case class State(editText: UnfinishedTitle)
class Backend($: BackendScope[Props, State]) {
val inputRef: RefSimple[dom.html.Input] =
Ref.apply[dom.html.Input]("input")
case class Callbacks(P: Props) {
val editFieldSubmit: Callback =
$.state.flatMap(_.editText.validated.fold(P.onDelete)(P.onUpdateTitle))
......@@ -38,10 +43,16 @@ object CTodoItem {
case _ => None
}
}
val cbs = Px.cbA($.props).map(Callbacks)
val cbs: Px[Callbacks] =
Px.cbA($.props).map(Callbacks)
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 = {
val cb = cbs.value()
......@@ -68,6 +79,7 @@ object CTodoItem {
)
),
<.input(
^.ref := inputRef,
^.className := "edit",
^.onBlur --> cb.editFieldSubmit,
^.onChange ==> editFieldChanged,
......@@ -78,10 +90,19 @@ object CTodoItem {
}
}
val component = ReactComponentB[Props]("CTodoItem")
.initialState_P(p => State(p.todo.title.editable))
.renderBackend[Backend].build
private val component =
ReactComponentB[Props]("TodoItem")
.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)
}
......@@ -7,9 +7,9 @@ import japgolly.scalajs.react.vdom.prefix_<^._
import org.scalajs.dom.ext.KeyCode
import org.scalajs.dom.html
object CTodoList {
object TodoList {
case class Props private[CTodoList] (
case class Props (
ctl: RouterCtl[TodoFilter],
model: TodoModel,
currentFilter: TodoFilter
......@@ -23,8 +23,10 @@ object CTodoList {
/**
* 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 r2 = Reusability.fn[State]((s1, s2) => s1.editing == s2.editing && (s1.todos eq s2.todos))
implicit val r1: Reusability[Props] =
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.
......@@ -56,7 +58,8 @@ object CTodoList {
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 =
id => $.modState(_.copy(editing = Some(id)))
......@@ -81,7 +84,7 @@ object CTodoList {
* 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()
val callbacks = cbs.value()
<.div(
<.h1("todos"),
<.header(
......@@ -114,7 +117,7 @@ object CTodoList {
<.ul(
^.className := "todo-list",
filteredTodos.map(todo =>
CTodoItem(CTodoItem.Props(
TodoItem(TodoItem.Props(
onToggle = P.model.toggleCompleted(todo.id),
onDelete = P.model.delete(todo.id),
onStartEditing = startEditing(todo.id),
......@@ -128,7 +131,7 @@ object CTodoList {
)
def footer(P: Props, activeCount: Int, completedCount: Int): ReactElement =
CFooter(CFooter.Props(
Footer(Footer.Props(
filterLink = P.ctl.link,
onClearCompleted = P.model.clearCompleted,
currentFilter = P.currentFilter,
......@@ -137,34 +140,35 @@ object CTodoList {
))
}
private val component = ReactComponentB[Props]("CTodoList")
/* state derived from the props */
.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.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
* 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.
*
* To understand how things are redrawn, change `shouldComponentUpdate` for
* either `shouldComponentUpdateWithOverlay` or `shouldComponentUpdateAndLog`
*/
.configure(Reusability.shouldComponentUpdate)
/**
* For performance reasons its important to only call `build` once for each component
*/
.build
private val component =
ReactComponentB[Props]("TodoList")
/* state derived from the props */
.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.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
* 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.
*
* To understand how things are redrawn, change `shouldComponentUpdate` for
* either `shouldComponentUpdateWithOverlay` or `shouldComponentUpdateAndLog`
*/
.configure(Reusability.shouldComponentUpdate)
/**
* For performance reasons its important to only call `build` once for each component
*/
.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))
}
......@@ -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))
def addTodo(title: Title): Callback =
......
......@@ -25,5 +25,6 @@ object TodoFilter {
object Active extends TodoFilter("active", "Active", !_.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