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