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._ ...@@ -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 =
ReactComponentB[Props]("Footer")
.stateless .stateless
.renderBackend[Backend] .renderBackend[Backend]
.build .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] =
RouterConfigDsl[TodoFilter].buildConfig { dsl =>
import 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] =
Reusability.fn[Props]((p1, p2) =>
(p1.todo eq p2.todo) && (p1.isEditing == p2.isEditing) (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 =
ReactComponentB[Props]("TodoItem")
.initialState_P(p => State(p.todo.title.editable)) .initialState_P(p => State(p.todo.title.editable))
.renderBackend[Backend].build .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)))
...@@ -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,7 +140,8 @@ object CTodoList { ...@@ -137,7 +140,8 @@ object CTodoList {
)) ))
} }
private val component = ReactComponentB[Props]("CTodoList") private val component =
ReactComponentB[Props]("TodoList")
/* state derived from the props */ /* state derived from the props */
.initialState_P(p => State(p.model.todos, None)) .initialState_P(p => State(p.model.todos, None))
.renderBackend[Backend] .renderBackend[Backend]
...@@ -165,6 +169,6 @@ object CTodoList { ...@@ -165,6 +169,6 @@ object CTodoList {
*/ */
.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