diff --git a/.gitignore b/.gitignore index 25a9322..1e57b0a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ bin/ .bloop .metals +.vscode diff --git a/build.sbt b/build.sbt index 52210b5..a166209 100644 --- a/build.sbt +++ b/build.sbt @@ -9,13 +9,14 @@ lazy val server = (project in file("server")) libraryDependencies ++= Seq( "com.vmunier" %% "scalajs-scripts" % "1.1.4", guice, - specs2 % Test + specs2 % Test, + "com.softwaremill.quicklens" %% "quicklens" % "1.6.0" ), // Compile the project before generating Eclipse files, so that generated .scala or .class files for views and routes are present EclipseKeys.preTasks := Seq(compile in Compile) ) .enablePlugins(PlayScala) - // .enablePlugins(SbtWeb) + .enablePlugins(SbtWeb) .enablePlugins(WebScalaJSBundlerPlugin) .dependsOn(sharedJvm) @@ -27,9 +28,15 @@ lazy val client = (project in file("client")) "org.scala-js" %%% "scalajs-dom" % "1.0.0", "com.github.japgolly.scalajs-react" %%% "core" % "1.7.0", "com.github.japgolly.scalajs-react" %%% "extra" % "1.7.0", + // "com.github.japgolly.scalajs-react" %%% "ext-monocle" % "1.7.0", + "com.github.japgolly.scalajs-react" %%% "ext-monocle-cats" % "1.7.0", + "com.softwaremill.quicklens" %%% "quicklens" % "1.5.0", + "com.github.julien-truffaut" %%% "monocle-core" % "2.0.4", + "com.github.julien-truffaut" %%% "monocle-macro" % "2.0.4", + "org.typelevel" %%% "cats-core" % "2.1.1" // "com.github.japgolly.scalajs-react" %%% "test" % "1.7.0", ), - scalacOptions ++= Seq("-Ymacro-annotations","-deprecation"), + scalacOptions ++= Seq("-Ymacro-annotations", "-deprecation"), useYarn := true, stFlavour := Flavour.Japgolly, Compile / npmDependencies ++= Seq( @@ -48,7 +55,11 @@ lazy val shared = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("shared")) .settings(commonSettings) - .jsConfigure(_.enablePlugins(ScalaJSWeb)) + // .jsConfigure(_.enablePlugins(ScalaJSWeb)) + .jsConfigure( + _.enablePlugins(ScalaJSBundlerPlugin) + // .enablePlugins(ScalablyTypedConverterPlugin) + ) lazy val sharedJvm = shared.jvm lazy val sharedJs = shared.js diff --git a/client/src/main/scala/com/example/playscalajsreact/ScalaJSExample.scala b/client/src/main/scala/com/example/playscalajsreact/ScalaJSExample.scala index a02d753..fd0a91d 100644 --- a/client/src/main/scala/com/example/playscalajsreact/ScalaJSExample.scala +++ b/client/src/main/scala/com/example/playscalajsreact/ScalaJSExample.scala @@ -5,86 +5,20 @@ import org.scalajs.dom import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.html_<^._ import japgolly.scalajs.react.extra._ -import com.example.playscalajsreact.model.HelloWorldSJSRComponent +import com.example.playscalajsreact.component.HelloWorldSJSRComponent import japgolly.scalajs.react.extra.router._ import japgolly.scalajs.react.extra.router.StaticDsl.Route - +import com.example.playscalajsreact.route.AppRouter +import com.example.playscalajsreact.component.Content +import com.example.playscalajsreact.component.Top2 object ScalaJSExample { def main(args: Array[String]): Unit = { - sealed trait Page - case object Home extends Page - case object Hello extends Page - case class Person(user: String) extends Page - case class ID(id: Int) extends Page - - case class Menu(name: String, route: Page) - - val mainMenu = Vector( - Menu("Home", Home), - Menu("Hello", Hello) - ) - - def layout(c: RouterCtl[Page], r: Resolution[Page]) = - <.ul( - // c.link(Home)("Home"), - // c.link(Hello)("Hello"), - mainMenu.toTagMod { item => - { - <.li( - ^.key := item.name, - item.name, - c setOnClick item.route - ) - } - }, - r.render() - ) - - val x = <.ol( - ^.id := "my-list", - ^.lang := "en", - ^.margin := 8.px, - <.li("Item 1"), - <.li("Item 2"), - HelloWorldSJSRComponent("Hello", 18) - ) - - val routerConfig = RouterConfigDsl[Page].buildConfig { dsl => - import dsl._ - import japgolly.scalajs.react.vdom.Implicits._ - - case class Item(category: String, itemId: java.util.UUID) extends Page - // val r = - // ("category" / string("[a-z]+") / "item" / int.caseClassDebug[ID]) - - // FIXME uncomment this block to get an error - Companion object not found for class Product - // case class Product(category: Int, item: Int) extends Page - // val r: Route[Product] = ("cat" / int / "item" / int).caseClass[Product] - - // val testRoute = - // ("user" / string("[a-z0-9]{1,20}") / "age" / int).pmap { - // case (a, b) => {} - // } - (emptyRule - | staticRoute(root, Home) ~> render(x) - // FIXME uncomment this block to get an error - Companion object not found for class Person - // | dynamicRouteCT("user" / string("[a-z0-9]{1,20}").caseClass[Person]) ~> dynRender( - // (page: Person) => { - // HelloWorldSJSRComponent(page.user, 0) - // } - // ) - | staticRoute("#hello", Hello) ~> render(<.div("TODO")) - | staticRedirect("#hey") ~> redirectToPage(Hello)(SetRouteVia.HistoryReplace)) - .notFound(redirectToPage(Home)(SetRouteVia.HistoryReplace)) - .renderWith(layout) - } - - // x.renderIntoDOM(dom.document.getElementById("app")) - val router = Router(BaseUrl.fromWindowOrigin / "index.html", routerConfig) - router().renderIntoDOM(dom.document.getElementById("app")) + val div = dom.document.createElement("div") + dom.document.body.appendChild(div) + Top2().renderIntoDOM(div) dom.document.getElementById("scalajsShoutOut").textContent = SharedMessages.itWorks diff --git a/client/src/main/scala/com/example/playscalajsreact/component/Content.scala b/client/src/main/scala/com/example/playscalajsreact/component/Content.scala new file mode 100644 index 0000000..14a69ad --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/component/Content.scala @@ -0,0 +1,63 @@ +package com.example.playscalajsreact.component + +import com.example.playscalajsreact.model.MyGlobalState +import japgolly.scalajs.react.vdom.VdomElement +import japgolly.scalajs.react.Callback +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import com.example.playscalajsreact.route.AppRouter +import org.scalajs.dom +import scala.scalajs.js +import com.example.playscalajsreact.model.User +import japgolly.scalajs.react.extra.StateSnapshot +import com.softwaremill.quicklens._ +import monocle.macros.Lenses + +object Content { + + import scala.concurrent.ExecutionContext.Implicits.global + + final case class State(myGlobalState: MyGlobalState = MyGlobalState()) + + final class Backend($ : BackendScope[_, State]) { + val modifyState = modify[State](_.myGlobalState) + val modifyUsername = modifyState andThenModify MyGlobalState.modifyUsername + var interval: js.UndefOr[js.timers.SetIntervalHandle] = + js.undefined + + def render(s: State): VdomElement = + // MyGlobalState.ctx.provide(s.myGlobalState) { + // <.div(AppRouter.router(AppRouter.Props(s.myGlobalState))) + // } + <.div + + val updateState = (s: State) => { + val direct = $.withEffectsImpure + direct.modState(modifyUsername.using(_ + "C")) + } + + val refresh = (s: State) => + Callback { + interval = js.timers.setInterval(1000) { + updateState(s) + } + } + + val clear = Callback { + interval foreach js.timers.clearInterval + interval = js.undefined + } + + } + + private val component = ScalaComponent + .builder[Unit]("content") + .initialState(State()) + .renderBackend[Backend] + .componentDidMount($ => $.backend.refresh($.state)) + .componentWillUnmount(_.backend.clear) + .build + + def apply() = component() +} + diff --git a/client/src/main/scala/com/example/playscalajsreact/model/HelloWorldComponent.scala b/client/src/main/scala/com/example/playscalajsreact/component/HelloWorldComponent.scala similarity index 79% rename from client/src/main/scala/com/example/playscalajsreact/model/HelloWorldComponent.scala rename to client/src/main/scala/com/example/playscalajsreact/component/HelloWorldComponent.scala index 114f050..53d399c 100644 --- a/client/src/main/scala/com/example/playscalajsreact/model/HelloWorldComponent.scala +++ b/client/src/main/scala/com/example/playscalajsreact/component/HelloWorldComponent.scala @@ -1,4 +1,4 @@ -package com.example.playscalajsreact.model +package com.example.playscalajsreact.component // import slinky.core.annotations.react // import slinky.core.StatelessComponent @@ -13,13 +13,13 @@ import japgolly.scalajs.react._ // } object HelloWorldSJSRComponent { - import japgolly.scalajs.react.vdom.html_<^._ + import japgolly.scalajs.react.vdom.all._ case class Props(name: String, age: Int) private val component = ScalaComponent .builder[Props]("HelloWorldComponent") - .render_P(p => { - <.p(p.name + p.age) + .render_P(props => { + p(props.name + " " + props.age) }) .build diff --git a/client/src/main/scala/com/example/playscalajsreact/component/IntEditor.scala b/client/src/main/scala/com/example/playscalajsreact/component/IntEditor.scala new file mode 100644 index 0000000..925fce1 --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/component/IntEditor.scala @@ -0,0 +1,25 @@ +package com.example.playscalajsreact.component + +object IntEditor { + import japgolly.scalajs.react._ + import japgolly.scalajs.react.vdom.html_<^._ + import japgolly.scalajs.react.MonocleReact._ + import japgolly.scalajs.react.extra._ + import monocle.macros.Lenses + + val component = ScalaComponent + .builder[StateSnapshot[Int]] + .render_P { stateSnapshot => + <.span( + ^.paddingLeft := "6ex", // leave some space for ReusabilityOverlay + <.button( + s"Current value is ${stateSnapshot.value}. Click to increment", + ^.onClick --> stateSnapshot.modState(_ + 1), + ) + ) + } + .configure(ReusabilityOverlay.install) + .build + + def apply(ss: StateSnapshot[Int]) = component(ss) +} \ No newline at end of file diff --git a/client/src/main/scala/com/example/playscalajsreact/component/MenuComponent.scala b/client/src/main/scala/com/example/playscalajsreact/component/MenuComponent.scala new file mode 100644 index 0000000..689d09e --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/component/MenuComponent.scala @@ -0,0 +1,54 @@ +package com.example.playscalajsreact.component +import com.example.playscalajsreact.model.MyGlobalState +import japgolly.scalajs.react.vdom.VdomElement +import japgolly.scalajs.react.Callback +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import com.example.playscalajsreact.route.AppRouter +import japgolly.scalajs.react.extra.router.RouterCtl +import com.example.playscalajsreact.route.Page +import com.example.playscalajsreact.route.Page._ +import japgolly.scalajs.react.extra.StateSnapshot +import com.example.playscalajsreact.model.User + +object MenuComponent { + case class State(myGlobalState: MyGlobalState = MyGlobalState()) + case class Props(state: StateSnapshot[MyGlobalState], c: RouterCtl[Page]) + + class Backend($ : BackendScope[Props, Unit]) { + + def render(props: Props): VdomElement = + { + val name = props.state.value.user.getOrElse(User.empty).username + <.ul( + Array( + Menu("Home", Home), + Menu("Hello", Hello), + Menu(name, Person(name, 0)), + Menu("Editor", Editor), + Menu("Test", Test) + ).toTagMod { item => + { + <.li( + ^.key := item.name, + <.a( + item.name, + props.c setOnClick item.route, + ^.color := "red" + ) + ) + } + } + ) + } + } + + private val component = ScalaComponent + .builder[Props]("menu") + // .initialState(State()) + .renderBackend[Backend] + // .componentDidMount($ => $.backend.refresh($.state)) + .build + + def apply(state: StateSnapshot[MyGlobalState], c: RouterCtl[Page]) = component(Props(state, c)) +} diff --git a/client/src/main/scala/com/example/playscalajsreact/component/Middle.scala b/client/src/main/scala/com/example/playscalajsreact/component/Middle.scala new file mode 100644 index 0000000..0efd971 --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/component/Middle.scala @@ -0,0 +1,58 @@ +package com.example.playscalajsreact.component + +import com.example.playscalajsreact.model.MyGlobalState +import japgolly.scalajs.react.vdom.VdomElement +import japgolly.scalajs.react.Callback +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import com.example.playscalajsreact.route.AppRouter +import org.scalajs.dom +import scala.scalajs.js +import com.example.playscalajsreact.model.User +import japgolly.scalajs.react.extra.StateSnapshot +import com.softwaremill.quicklens._ +import monocle.macros.Lenses +import com.example.playscalajsreact.model._ +import japgolly.scalajs.react.MonocleReact._ + +object Middle { + + final case class Props(name: String, ss: StateSnapshot[Data]) { + @inline def render: VdomElement = Comp(this) + } + + implicit def reusability: Reusability[Props] = + Reusability.derive + + final class Backend($ : BackendScope[Props, Unit]) { + + // Method 2: StateSnapshot.withReuse.zoomL.prepareViaProps + // Notice that we're using a normal lens here instead of a Reusable[lens] + private val ssStrFn = + StateSnapshot.withReuse.zoomL(Data.str).prepareViaProps($)(_.ss) + + def render(p: Props): VdomElement = { + + // Method 1: ss.withReuse.zoomStateL + val ssI: StateSnapshot[Int] = p.ss.zoomStateL(Data.reusableLens.int) + + // Method 2: StateSnapshot.withReuse.zoomL.prepareViaProps + val ssS: StateSnapshot[String] = + ssStrFn(p.ss.value) + + <.div( + <.h3(p.name), + <.div("IntEditor: ", IntEditor(ssI)) + // <.div("TextEditor: ", TextEditor(ssS), ^.marginTop := "0.6em")) + ) + } + } + + val Comp = ScalaComponent + .builder[Props] + .renderBackend[Backend] + .configure(Reusability.shouldComponentUpdate) + .build + + def apply(_props: Props): VdomElement = { Comp(_props) } +} diff --git a/client/src/main/scala/com/example/playscalajsreact/component/NameChanger.scala b/client/src/main/scala/com/example/playscalajsreact/component/NameChanger.scala new file mode 100644 index 0000000..5b219db --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/component/NameChanger.scala @@ -0,0 +1,47 @@ +package com.example.playscalajsreact.component + +import japgolly.scalajs.react.vdom.VdomElement +import japgolly.scalajs.react.Callback +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import com.example.playscalajsreact.route.AppRouter +import org.scalajs.dom +import scala.scalajs.js +import com.example.playscalajsreact.model.User +import japgolly.scalajs.react.extra.StateSnapshot +import com.softwaremill.quicklens._ +import monocle.macros.Lenses + +object NameChanger { + import japgolly.scalajs.react.MonocleReact._ + + @Lenses + case class Name(firstName: String, surname: String) + + val NameChanger = ScalaComponent + .builder[StateSnapshot[String]] + .render_P { stateSnapshot => + <.input.text( + ^.value := stateSnapshot.value, + ^.onChange ==> ((e: ReactEventFromInput) => + stateSnapshot.setState(e.target.value) + ) + ) + } + .build + + val Main = ScalaComponent + .builder[Unit] + .initialState(Name("John", "Wick")) + .render { $ => + val name = $.state + val firstNameV = StateSnapshot.zoomL(Name.firstName).of($) + val surnameV = StateSnapshot.zoomL(Name.surname).of($) + <.div( + <.label("First name:", NameChanger(firstNameV)), + <.label("Surname:", NameChanger(surnameV)), + <.p(s"My name is ${name.surname}, ${name.firstName} ${name.surname}.") + ) + } + .build +} \ No newline at end of file diff --git a/client/src/main/scala/com/example/playscalajsreact/component/Top.scala b/client/src/main/scala/com/example/playscalajsreact/component/Top.scala new file mode 100644 index 0000000..2c6d16b --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/component/Top.scala @@ -0,0 +1,170 @@ +package com.example.playscalajsreact.component + +import com.example.playscalajsreact.model.MyGlobalState +import japgolly.scalajs.react.vdom.VdomElement +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import com.example.playscalajsreact.route.AppRouter +import com.example.playscalajsreact.model.User +import japgolly.scalajs.react.extra.StateSnapshot +// import com.softwaremill.quicklens._ +import com.example.playscalajsreact.model._ + +object Top { + + final class Backend($ : BackendScope[Unit, Data]) { + private val setStateFn = + StateSnapshot.withReuse.prepareVia($) + + def render(state: Data): VdomElement = { + val ss = setStateFn(state) + // Middle.Props("Demo", ss).render + Middle(Middle.Props("Demo", ss)) + } + } + + val Comp = ScalaComponent + .builder[Unit] + .initialState(Data(123, "hello")) + .renderBackend[Backend] + .build +} + +object Top2 { + import japgolly.scalajs.react.vdom.all._ + import japgolly.scalajs.react.MonocleReact._ + + final class Backend($ : BackendScope[Unit, MyGlobalState]) { + private val setStateFn = + StateSnapshot.withReuse.prepareVia($) + + def render(state: MyGlobalState): VdomElement = { + // val ss = StateSnapshot.zoomL(MyGlobalState.user)(state).setStateVia($) + + // val ss2 = ss.xmapState(u => Snappy.State(u))(_.user) + // div(Snappy.Props(ss2).render) + val ss = setStateFn(state) + + div( + // Middle2.Props("Middle2", ss).render, + AppRouter.router(AppRouter.Props(ss)), + "Value: ", + state.user.map(_.username) + ) + } + } + + val Top2Component = ScalaComponent + .builder[Unit]("Top2") + .initialState(MyGlobalState(Some(User("testuser")))) + // .initialState(MyGlobalState.empty) + .renderBackend[Backend] + .build + + def apply(): VdomElement = Top2Component() +} + +object Middle2 { + import japgolly.scalajs.react.MonocleReact._ + import monocle.macros.syntax.lens._ + import monocle.std.option._ + import monocle.macros.GenIso + import cats.implicits._ + + // val navigateToUsername = GenIso[MyGlobalState, Option[User]] + // .composePrism(GenIso[User, String].asPrism.below[Option]) + + val navigateToUsername = + MyGlobalState.user.composePrism(GenIso[User, String].asPrism.below[Option]) + + final case class Props(name: String, ss: StateSnapshot[MyGlobalState]) { + @inline def render: VdomElement = Comp(this) + } + + implicit def reusability: Reusability[Props] = + Reusability.derive + + final class Backend($ : BackendScope[Props, Unit]) { + + // Method 2: StateSnapshot.withReuse.zoomL.prepareViaProps + // Notice that we're using a normal lens here instead of a Reusable[lens] + private val ssStrFn = + StateSnapshot.withReuse.zoomL(MyGlobalState.user).prepareViaProps($)(_.ss) + + def render(p: Props): VdomElement = { + val x = p.ss.zoomStateO(navigateToUsername) + val y = x.map(_.xmapState(_ => 1)(_ => None)) + + // Method 1: ss.withReuse.zoomStateL + // val ssI: StateSnapshot[Int] = p.ss.zoomStateL(Data.reusableLens.int) + + // Method 2: StateSnapshot.withReuse.zoomL.prepareViaProps + // val ssS: StateSnapshot[String] = + // ssStrFn(p.ss.value) + + val ss4 = p.ss.zoomStateL(MyGlobalState.user) + // val ss5 = p.ss.zoomStateO(navigateToUsername.asOptional) + // val ss6 = + // ss5.map(_.xmapState(_.map(n => User(n)))(_.map(u => u.username))) + // ss5.foreach(_.modState(e => e)) + // val x = p.ss.value.lens(_.user.getOrElse(User.empty).username) + // p.ss.zoomStateO() + + val ss2 = ss4.xmapState(u => Snappy.State(u))(_.user) + + <.div( + <.h3(p.name), + <.div("Snappy", Snappy.Props(ss2).render) + ) + } + } + + val Comp = ScalaComponent + .builder[Props] + .renderBackend[Backend] + // .configure(Reusability.shouldComponentUpdate) + .build + + def apply(_props: Props): VdomElement = { Comp(_props) } +} + +object Snappy { + + final case class Props(state: StateSnapshot[State]) { + @inline def render: VdomElement = Component(this) + } + + //implicit val reusabilityProps: Reusability[Props] = + // Reusability.derive + + final case class State(user: Option[User]) + + object State { + def empty = State(user = None) + + //implicit val reusability: Reusability[State] = + // Reusability.derive + } + + final class Backend($ : BackendScope[Props, Unit]) { + import com.softwaremill.quicklens._ + def render(p: Props): VdomNode = { + val s = p.state.value + <.div("Test", s.user.map(u => { + <.div( + "Username", + u.username, + ^.onClick --> p.state.modState( + _.modify(_.user.each.username).using(_ + "c") + ) + ) + })) + } + } + + val Component = ScalaComponent + .builder[Props]("Snappy") + .renderBackend[Backend] + //.configure(Reusability.shouldComponentUpdate) + .build +} diff --git a/client/src/main/scala/com/example/playscalajsreact/model/Data.scala b/client/src/main/scala/com/example/playscalajsreact/model/Data.scala new file mode 100644 index 0000000..d06007d --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/model/Data.scala @@ -0,0 +1,19 @@ +package com.example.playscalajsreact.model + +import monocle.macros.Lenses +import japgolly.scalajs.react.Reusability +import japgolly.scalajs.react.Reusable + +@Lenses +final case class Data(int: Int, str: String) + +object Data { + implicit val reusability: Reusability[Data] = Reusability.derive + + // Here we wrap the lenses in Reusable.byRef so that React can compare setState/modState functions and know when its + // it's got the same lens as a previous render. This is required to make [Method 1] work with Reusability + object reusableLens { + val int = Reusable.byRef(Data.int) + val str = Reusable.byRef(Data.str) + } +} \ No newline at end of file diff --git a/client/src/main/scala/com/example/playscalajsreact/model/GlobalState.scala b/client/src/main/scala/com/example/playscalajsreact/model/GlobalState.scala new file mode 100644 index 0000000..cb847a7 --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/model/GlobalState.scala @@ -0,0 +1,19 @@ +package com.example.playscalajsreact.model + +import japgolly.scalajs.react.feature.Context +import japgolly.scalajs.react.React +import com.softwaremill.quicklens._ +import monocle.macros.Lenses +import japgolly.scalajs.react.Reusability + +@Lenses +case class MyGlobalState(user: Option[User] = None, name: String = "") + +object MyGlobalState { + val ctx: Context[MyGlobalState] = React.createContext(MyGlobalState()) + val modifyUsername = modify[MyGlobalState](_.user.each.username) + + implicit val reusability: Reusability[MyGlobalState] = Reusability.derive + + def empty = MyGlobalState(None, "") +} \ No newline at end of file diff --git a/client/src/main/scala/com/example/playscalajsreact/model/SnapshotTest.scala b/client/src/main/scala/com/example/playscalajsreact/model/SnapshotTest.scala new file mode 100644 index 0000000..c3f4669 --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/model/SnapshotTest.scala @@ -0,0 +1,53 @@ +package com.example.playscalajsreact.model + +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ + +object Top { + + final case class Props() { + @inline def render: VdomElement = Component(this) + } + + //implicit val reusabilityProps: Reusability[Props] = + // Reusability.derive + + final class Backend($: BackendScope[Props, Unit]) { + def render(p: Props): VdomNode = + <.div + } + + val Component = ScalaComponent.builder[Props]("Top") + .renderBackend[Backend] + //.configure(Reusability.shouldComponentUpdate) + .build + + def apply() = Component(Props()) +} + +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.all._ + +object Test2 { + + final case class Props() + + //implicit val reusabilityProps: Reusability[Props] = + // Reusability.derive + + final class Backend($: BackendScope[Props, Unit]) { + def render(p: Props): VdomNode = + <.div + } + + private val Test2Component = ScalaComponent.builder[Props]("Test2") + .renderBackend[Backend] + //.configure(Reusability.shouldComponentUpdate) + .build + + def apply(): VdomElement = Test2Component(Props()) +} + +object Test3 { + Test2() +} diff --git a/client/src/main/scala/com/example/playscalajsreact/model/User.scala b/client/src/main/scala/com/example/playscalajsreact/model/User.scala new file mode 100644 index 0000000..872178d --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/model/User.scala @@ -0,0 +1,13 @@ +package com.example.playscalajsreact.model + +import monocle.macros.Lenses +import japgolly.scalajs.react.Reusability + + +@Lenses +case class User(username: String) + +object User { + def empty = User("") + implicit val reusability: Reusability[User] = Reusability.derive +} \ No newline at end of file diff --git a/client/src/main/scala/com/example/playscalajsreact/route/AppRouter.scala b/client/src/main/scala/com/example/playscalajsreact/route/AppRouter.scala new file mode 100644 index 0000000..3296201 --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/route/AppRouter.scala @@ -0,0 +1,61 @@ +package com.example.playscalajsreact.route + +import org.scalajs.dom +import japgolly.scalajs.react._ +import japgolly.scalajs.react.vdom.html_<^._ +import japgolly.scalajs.react.extra._ +import japgolly.scalajs.react.extra.router._ +import com.example.playscalajsreact.component.HelloWorldSJSRComponent +import com.example.playscalajsreact.component.MenuComponent +import com.example.playscalajsreact.component.Top +import com.example.playscalajsreact.component.Top2 +import com.example.playscalajsreact.model.MyGlobalState +import com.example.playscalajsreact.component.Middle2 + + + +object AppRouter { + import com.example.playscalajsreact.route.Page._ + + final case class Props(state: StateSnapshot[MyGlobalState]) + + private def layout(c: RouterCtl[Page], r: ResolutionWithProps[Page, Props])( + appState: Props + ) = + <.div( + MenuComponent(appState.state, c), + r.renderP(appState) + ) + + val x = <.ol( + ^.id := "my-list", + ^.lang := "en", + ^.margin := 8.px, + <.li("Item 1"), + <.li("Item 2") + // HelloWorldSJSRComponent("Hello", 18) + ) + + val routerConfig = RouterWithPropsConfigDsl[Page, Props].buildConfig { dsl => + import dsl._ + (emptyRule + | staticRoute(root, Home) ~> render(x) + | dynamicRouteCT( + ("#user" / string("[a-zA-Z0-9]{1,20}") / "age" / int) + .caseClass[Person] + ) ~> dynRender((page: Person) => { + HelloWorldSJSRComponent(page.user, page.age) + }) + | staticRoute("#hello", Hello) ~> render(<.div("TODO")) + | staticRoute("#editor", Editor) ~> render(Top.Comp()) + | staticRoute("#test", Test) ~> renderP(p => Middle2.Props("Aege", p.state).render) + | staticRedirect("#hey") ~> redirectToPage(Hello)( + SetRouteVia.HistoryReplace + )) + .notFound(redirectToPage(Home)(SetRouteVia.HistoryReplace)) + .renderWithP(layout) + } + + val router = + RouterWithProps(BaseUrl.fromWindowOrigin / "index", AppRouter.routerConfig) +} diff --git a/client/src/main/scala/com/example/playscalajsreact/route/Page.scala b/client/src/main/scala/com/example/playscalajsreact/route/Page.scala new file mode 100644 index 0000000..a378e3e --- /dev/null +++ b/client/src/main/scala/com/example/playscalajsreact/route/Page.scala @@ -0,0 +1,15 @@ +package com.example.playscalajsreact.route + +sealed trait Page +object Page { + case object Home extends Page + case object Hello extends Page + case class Person(user: String, age: Int) extends Page + case class ID(id: Int) extends Page + + case class Menu(name: String, route: Page) + case class Product(category: Int, item: Int) extends Page + case class Item(category: String, itemId: java.util.UUID) extends Page + case object Editor extends Page + case object Test extends Page +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 7949a3f..89c72ca 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -13,7 +13,7 @@ addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta13") // Use Scala.js v1.x addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.0.11") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.1") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.0") // addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.18.0") addSbtPlugin("ch.epfl.scala" % "sbt-web-scalajs-bundler" % "0.18.0") diff --git a/server/conf/routes b/server/conf/routes index 6a45f0a..6689814 100644 --- a/server/conf/routes +++ b/server/conf/routes @@ -4,6 +4,7 @@ # Home page GET / com.example.playscalajsreact.controllers.Application.index +GET /index com.example.playscalajsreact.controllers.Application.index # Prefix must match `play.assets.urlPrefix` GET /assets/*file controllers.Assets.at(file) diff --git a/shared/src/main/scala/com/example/playscalajsreact/shared/SharedMessages.scala b/shared/src/main/scala/com/example/playscalajsreact/shared/SharedMessages.scala index 8fbc606..bb05daf 100644 --- a/shared/src/main/scala/com/example/playscalajsreact/shared/SharedMessages.scala +++ b/shared/src/main/scala/com/example/playscalajsreact/shared/SharedMessages.scala @@ -1,5 +1,5 @@ package com.example.playscalajsreact.shared object SharedMessages { - def itWorks = "It works!" + def itWorks = "It works too!" }