From fbe1403aa849426ff8f7e8f87c7657c2efe90b2d Mon Sep 17 00:00:00 2001 From: Shogo Wada Date: Sat, 15 Apr 2017 18:07:03 -0500 Subject: [PATCH] Migrate to react-router v4 (#27) * Correct props - Create react element via <(ReactClassSpec)(attribute: Any*)(contents: Any*) method - Specify props via ^.wrapped attribute - Use props.children to access children - Use props.router, props.location, etc to access router props * Fix travis CI * Use Chrome instead * Fix travis CI * Fix travis CI * Remove children method It is replaced by props.children * Create react-router-dom facade * Drop 2.11 support It was having issue resolving class for WebBrowser for no obvious reason. * Update README.md * Update documentations Close #26 * Drop Scala 2.11 from travis * Update README.md * Remove comments * Update documents * Update README.md * Update Main.scala * Loosen selenium version Travis is having issue finding 3.3.1 * Try installing newer chromdriver * Revert manually downloding chromedriver * Install google-chrome-stable * Revert "Install google-chrome-stable" This reverts commit d1f45c3f82986cdb30f6c909ac0d4767dab98937. * Install Chrome * Use chromedriver 2.28 According to https://bugs.chromium.org/p/chromedriver/issues/detail?id=1467, it has the fix for alert hanging. * Ignore unexpected alert * Use singleton FirefoxDriver * Install geckodriver * Fix gecko download path * Do not use trusty --- .travis.yml | 10 +- README.md | 41 ++- build.sbt | 32 ++- core/README.md | 12 +- .../scalajs/reactjs/Converters.scala | 21 -- .../shogowada/scalajs/reactjs/React.scala | 30 +-- .../shogowada/scalajs/reactjs/ReactDOM.scala | 24 +- .../scalajs/reactjs/VirtualDOM.scala | 66 +++-- .../classes/specs/ReactClassSpec.scala | 102 ++------ .../reactjs/elements/ReactHTMLElements.scala | 30 --- .../reactjs/events/FormSyntheticEvent.scala | 2 - example/README.md | 117 ++++++++- .../reactjs/example/helloworld/Main.scala | 11 +- .../example/interactive/helloworld/Main.scala | 32 +-- .../reactjs/example/lifecycle/Main.scala | 12 +- .../reactjs/example/routing/Main.scala | 187 ++++++------- .../scalajs/reactjs/example/BaseTest.scala | 19 ++ .../CustomVirtualDOMTest.scala | 12 +- .../example/helloworld/HelloWorldTest.scala | 16 +- .../HelloWorldFunctionTest.scala | 16 +- .../InteractiveHelloWorldTest.scala | 30 +-- .../example/lifecycle/LifecycleTest.scala | 12 +- .../reactjs/example/routing/RoutingTest.scala | 64 ++--- .../reactjs/example/style/StyleTest.scala | 12 +- .../reactjs/example/todoapp/TodoAppTest.scala | 32 +-- .../todoappredux/TodoAppReduxTest.scala | 25 +- .../example/todoappredux/Actions.scala | 2 +- .../todoappredux/ContainerComponents.scala | 36 +-- .../reactjs/example/todoappredux/Main.scala | 20 +- .../PresentationalComponents.scala | 45 ++-- .../example/todoappredux/Reducer.scala | 50 ++-- example/todo-app/README.md | 246 ------------------ .../reactjs/example/todoapp/Main.scala | 82 +++--- redux/README.md | 11 +- .../reactjs/redux/ContainerComponent.scala | 47 ++++ .../scalajs/reactjs/redux/ReactRedux.scala | 131 +++------- .../scalajs/reactjs/redux/Redux.scala | 1 - .../reactjs/router/dom/RouterDOM.scala | 75 ++++++ router/README.md | 11 +- .../scalajs/reactjs/router/History.scala | 8 - .../scalajs/reactjs/router/Location.scala | 13 + .../scalajs/reactjs/router/Match.scala | 11 + .../reactjs/router/RoutedReactClassSpec.scala | 40 --- .../scalajs/reactjs/router/Router.scala | 117 +++------ .../scalajs/reactjs/router/RouterProps.scala | 13 + .../scalajs/reactjs/router/WithRouter.scala | 6 +- 46 files changed, 788 insertions(+), 1144 deletions(-) delete mode 100644 core/src/main/scala/io/github/shogowada/scalajs/reactjs/Converters.scala create mode 100644 example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/BaseTest.scala delete mode 100644 example/todo-app/README.md create mode 100644 redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ContainerComponent.scala create mode 100644 router-dom/src/main/scala/io/github/shogowada/scalajs/reactjs/router/dom/RouterDOM.scala create mode 100644 router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Location.scala create mode 100644 router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Match.scala delete mode 100644 router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RoutedReactClassSpec.scala create mode 100644 router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RouterProps.scala diff --git a/.travis.yml b/.travis.yml index ca62cfc..01aaea1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,25 @@ language: scala scala: - - 2.11.8 - 2.12.1 jdk: - oraclejdk8 +addons: + firefox: "latest" + before_script: + - wget https://github.com/mozilla/geckodriver/releases/download/v0.15.0/geckodriver-v0.15.0-linux64.tar.gz + - mkdir geckodriver + - tar -xzf geckodriver-v0.15.0-linux64.tar.gz -C geckodriver + - export PATH=$PATH:$PWD/geckodriver - "export DISPLAY=:99" - "sh -e /etc/init.d/xvfb start" - sleep 3 # give xvfb some time to start - ". $HOME/.nvm/nvm.sh" - "nvm install node" - "nvm use node" - - "node --version" - - "npm --version" script: - sbt ++$TRAVIS_SCALA_VERSION fastOptJS::webpack diff --git a/README.md b/README.md index 5c3fd67..9461b4e 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,27 @@ [![Build Status](https://travis-ci.org/shogowada/scalajs-reactjs.svg?branch=master)](https://travis-ci.org/shogowada/scalajs-reactjs) -Develop React JS applications with Scala. It is compatible with Scala 2.11, 2.12, and Scala.js 0.6.14+. +Develop React applications with Scala. It is compatible with Scala 2.12 and Scala.js 0.6.14. + +Optionally include react-router and react-redux facades, too. ## Quick Look ```scala import io.github.shogowada.scalajs.reactjs.VirtualDOM._ -class HelloWorld extends StatelessReactClassSpec[HelloWorld.Props] { - override def render() = <.div(^.id := "hello-world")(s"Hello, ${props.name}!") +class HelloWorld extends StatelessReactClassSpec[HelloWorld.WrappedProps] { + override def render() = <.div(^.id := "hello-world")(s"Hello, ${props.wrapped.name}!") } object HelloWorld { - case class Props(name: String) + case class WrappedProps(name: String) - def apply(props: Props): ReactElement = (new HelloWorld) (props)() + def apply() = new HelloWorld() } val mountNode = dom.document.getElementById("mount-node") -ReactDOM.render(HelloWorld(HelloWorld.Props("World")), mountNode) +ReactDOM.render(<(HelloWorld())(^.wrapped := HelloWorld.WrappedProps("World"))(), mountNode) ``` You can also use a pure function to render: @@ -29,14 +31,11 @@ You can also use a pure function to render: import io.github.shogowada.scalajs.reactjs.VirtualDOM._ object HelloWorld { - case class Props(name: String) - - def apply(props: Props): ReactElement = <.div(^.id := "hello-world")(s"Hello, ${props.name}!") + def apply(name: String): ReactElement = <.div(^.id := "hello-world")(s"Hello, ${name}!") } val mountNode = dom.document.getElementById("mount-node") -ReactDOM.render(HelloWorld(HelloWorld.Props("World")), mountNode) - +ReactDOM.render(HelloWorld("World"), mountNode) ``` ## How to Use @@ -45,22 +44,16 @@ ReactDOM.render(HelloWorld(HelloWorld.Props("World")), mountNode) 2. Depend on the libraries. ``` libraryDependencies ++= Seq( - "io.github.shogowada" %%% "scalajs-reactjs" % "0.7.1", // For react facade - "io.github.shogowada" %%% "scalajs-reactjs-router" % "0.7.1", // Optional. For react-router facade - "io.github.shogowada" %%% "scalajs-reactjs-redux" % "0.7.1" // Optional. For react-redux facade + "io.github.shogowada" %%% "scalajs-reactjs" % "0.8.0", // For react facade + "io.github.shogowada" %%% "scalajs-reactjs-router-dom" % "0.8.0", // Optional. For react-router-dom facade + "io.github.shogowada" %%% "scalajs-reactjs-redux" % "0.8.0" // Optional. For react-redux facade ) ``` ## Examples -- [TODO App](./example/todo-app) -- [Routing](./router) -- [Redux](./redux) +- [Basics](./example) +- [TODO App](./example/todo-app/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/Main.scala) +- [Routing](./example/routing/src/main/scala/io/github/shogowada/scalajs/reactjs/example/routing/Main.scala) +- [Redux](./example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux) - [I don't like `<` and `^`. How can I change them?](./example/custom-virtual-dom) -- [All Examples](./example) - -## API References - -- [react facade](./core) -- [react-router facade](./router) -- [react-redux facade](./redux) diff --git a/build.sbt b/build.sbt index c4ea23c..ae99bc6 100644 --- a/build.sbt +++ b/build.sbt @@ -1,13 +1,16 @@ val CreateReactClassVersion = "^15.5.1" val ReactVersion = "^15.5.3" val ReactReduxVersion = "^5.0.3" -val ReactRouterVersion = "^3.0.0" +val ReactRouterVersion = "^4.0.0" val ReduxVersion = "^3.6.0" val WebpackVersion = "^2.3.2" val StaticTagsVersion = "[2.4.0,3.0.0[" -crossScalaVersions := Seq("2.11.8", "2.12.1") +val SeleniumVersion = "[3.0.0,4.0.0[" +val ScalaTestVersion = "[3.1.0,4.0.0[" + +crossScalaVersions := Seq("2.12.1") publishTo := { val nexus = "https://oss.sonatype.org/" @@ -21,7 +24,7 @@ publishArtifact := false val commonSettings = Seq( organization := "io.github.shogowada", name := "scalajs-reactjs", - version := "0.7.2-SNAPSHOT", + version := "0.8.0", licenses := Seq("MIT" -> url("https://opensource.org/licenses/MIT")), homepage := Some(url("https://github.com/shogowada/scalajs-reactjs")), scalaVersion := "2.12.1", @@ -83,6 +86,20 @@ lazy val router = project.in(file("router")) .enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin) .dependsOn(core) +lazy val routerDom = project.in(file("router-dom")) + .settings(commonSettings: _*) + .settings( + name += "-router-dom", + npmDependencies in Compile ++= Seq( + "react-router-dom" -> ReactRouterVersion + ), + (webpack in(Compile, fastOptJS)) := Seq(), + (webpack in(Compile, fullOptJS)) := Seq(), + publishArtifact := true + ) + .enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin) + .dependsOn(core, router) + lazy val redux = project.in(file("redux")) .settings(commonSettings: _*) .settings( @@ -141,7 +158,7 @@ lazy val exampleRouting = project.in(file("example") / "routing") name += "-routing" ) .enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin) - .dependsOn(core, router) + .dependsOn(core, routerDom) lazy val exampleStyle = project.in(file("example") / "style") .settings(exampleCommonSettings: _*) @@ -182,10 +199,10 @@ lazy val exampleTest = project.in(file("example") / "test") .settings( name += "-example-test", libraryDependencies ++= Seq( - "org.eclipse.jetty" % "jetty-server" % "9.3+", - "org.seleniumhq.selenium" % "selenium-java" % "2.+", + "org.eclipse.jetty" % "jetty-server" % "9.3.+", + "org.seleniumhq.selenium" % "selenium-java" % SeleniumVersion, - "org.scalatest" %% "scalatest" % "3.+" + "org.scalatest" %% "scalatest" % ScalaTestVersion ), javaOptions ++= Seq( s"-Dtarget.path.custom-virtual-dom=${(crossTarget in exampleCustomVirtualDOM).value}", @@ -197,6 +214,7 @@ lazy val exampleTest = project.in(file("example") / "test") s"-Dtarget.path.style=${(crossTarget in exampleStyle).value}", s"-Dtarget.path.todo-app=${(crossTarget in exampleTodoApp).value}", s"-Dtarget.path.todo-app-redux=${(crossTarget in exampleTodoAppRedux).value}", + // Just to build them s"-Ddummy.custom-virtual-dom=${(webpack in fastOptJS in Compile in exampleCustomVirtualDOM).value}", s"-Ddummy.helloworld=${(webpack in fastOptJS in Compile in exampleHelloWorld).value}", s"-Ddummy.helloworld-function=${(webpack in fastOptJS in Compile in exampleHelloWorldFunction).value}", diff --git a/core/README.md b/core/README.md index 061f780..59f1557 100644 --- a/core/README.md +++ b/core/README.md @@ -1,11 +1,3 @@ -# Facade for react and react-dom +# scalajs-reactjs -## API References - -- [`React`](./src/main/scala/io/github/shogowada/scalajs/reactjs/React.scala) -- [`ReactDOM`](./src/main/scala/io/github/shogowada/scalajs/reactjs/ReactDOM.scala) -- [`VirtualDOM`](./src/main/scala/io/github/shogowada/scalajs/reactjs/VirtualDOM.scala) -- [`ReactClassSpec`](./src/main/scala/io/github/shogowada/scalajs/reactjs/classes/specs/ReactClassSpec.scala) -- [`ReactHTMLElements`](./src/main/scala/io/github/shogowada/scalajs/reactjs/elements/ReactHTMLElements.scala) -- [`SyntheticEvent`](./src/main/scala/io/github/shogowada/scalajs/reactjs/events/SyntheticEvent.scala) -- [`FormSyntheticEvent`](./src/main/scala/io/github/shogowada/scalajs/reactjs/events/FormSyntheticEvent.scala) +A facade for react and react-dom diff --git a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/Converters.scala b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/Converters.scala deleted file mode 100644 index 6bebec7..0000000 --- a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/Converters.scala +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.shogowada.scalajs.reactjs - -import scala.scalajs.js - -object Converters { - - implicit class ScalaFunction0[T](function: () => T) { - def asJs: js.Function0[T] = { - val jsFunction: js.Function0[T] = function - jsFunction - } - } - - implicit class ScalaFunction1[T0, R](function: T0 => R) { - def asJs: js.Function1[T0, R] = { - val jsFunction: js.Function1[T0, R] = function - jsFunction - } - } - -} diff --git a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/React.scala b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/React.scala index 144139f..d83e78e 100644 --- a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/React.scala +++ b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/React.scala @@ -7,39 +7,15 @@ import io.github.shogowada.scalajs.reactjs.elements.ReactElement import scala.scalajs.js import scala.scalajs.js.annotation.JSImport -/** Facade for react */ object React { - - /** Returns [[ReactClass]] created by given [[ReactClassSpec]] */ def createClass[Props, State](spec: ReactClassSpec[Props, State]): ReactClass = NativeCreateReactClass(spec.asNative) - /** Returns [[ReactElement]] created by given tag name, attributes, and children */ def createElement(tagName: String, attributes: js.Any, children: js.Any*): ReactElement = NativeReact.createElement(tagName, attributes, children: _*) - /** Returns [[ReactElement]] created by given [[ReactClassSpec]] */ - def createElement[Props, State](spec: ReactClassSpec[Props, State]): ReactElement = { - val reactClass = createClass(spec) - NativeReact.createElement(reactClass) - } - - /** Returns [[ReactElement]] created by given [[ReactClassSpec]] and its props */ - def createElement[Props, State](spec: ReactClassSpec[Props, State], props: Props): ReactElement = { - val reactClass = createClass(spec) - NativeReact.createElement(reactClass, ReactClassSpec.propsToNative(props)) - } - - /** Returns [[ReactElement]] created by given [[ReactClassSpec]], its props, and children */ - def createElement[Props, State](spec: ReactClassSpec[Props, State], props: Props, children: js.Any*): ReactElement = { - val reactClass = createClass(spec) - NativeReact.createElement(reactClass, ReactClassSpec.propsToNative(props), children: _*) - } - - /** Returns [[ReactElement]] created by given [[ReactClass]], its props, and children */ - def createElement(reactClass: ReactClass, props: js.Any, children: js.Any*): ReactElement = { + def createElement(reactClass: ReactClass, props: js.Any, children: js.Any*): ReactElement = NativeReact.createElement(reactClass, props, children: _*) - } def createElement(reactClass: ReactClass): ReactElement = NativeReact.createElement(reactClass) @@ -52,9 +28,7 @@ object NativeReact extends js.Object { def createElement(reactClass: ReactClass): ReactElement = js.native - def createElement(reactClass: ReactClass, attributes: js.Any): ReactElement = js.native - - def createElement(reactClass: ReactClass, props: js.Any, children: js.Any*): ReactElement = js.native + def createElement(reactClass: ReactClass, attributes: js.Any, children: js.Any*): ReactElement = js.native } @js.native diff --git a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/ReactDOM.scala b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/ReactDOM.scala index b5c89e1..dd52139 100644 --- a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/ReactDOM.scala +++ b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/ReactDOM.scala @@ -1,33 +1,13 @@ package io.github.shogowada.scalajs.reactjs -import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec import io.github.shogowada.scalajs.reactjs.elements.ReactElement import org.scalajs.dom import scala.scalajs.js import scala.scalajs.js.annotation.JSImport -/** Facade for react-dom */ -object ReactDOM { - /** Mounts [[ReactClassSpec]] to given node */ - def render[Props, State](reactClassSpec: ReactClassSpec[Props, State], node: dom.Node): Unit = { - render(React.createElement(reactClassSpec), node) - } - - /** Mounts [[ReactClassSpec]] with its props to given node */ - def render[Props, State](reactClassSpec: ReactClassSpec[Props, State], props: Props, node: dom.Node): Unit = { - render(React.createElement(reactClassSpec, props), node) - } - - /** Mounts [[ReactElement]] to given node */ - def render(element: ReactElement, node: dom.Node): Unit = { - NativeReactDOM.render(element, node) - } -} - @js.native @JSImport("react-dom", JSImport.Namespace) -object NativeReactDOM extends js.Object { - - def render(element: ReactElement, node: dom.Node): Unit = js.native +object ReactDOM extends js.Object { + def render(element: ReactElement, node: dom.Node): js.Any = js.native } diff --git a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/VirtualDOM.scala b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/VirtualDOM.scala index 04a0049..c26d4a0 100644 --- a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/VirtualDOM.scala +++ b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/VirtualDOM.scala @@ -1,8 +1,10 @@ package io.github.shogowada.scalajs.reactjs import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMAttributes.Type.AS_IS +import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMElements.ReactClassElementSpec import io.github.shogowada.scalajs.reactjs.classes.ReactClass import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec +import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec.Render import io.github.shogowada.scalajs.reactjs.elements.{ReactElement, ReactHTMLElement} import io.github.shogowada.scalajs.reactjs.events._ import io.github.shogowada.statictags.AttributeValueType.AttributeValueType @@ -12,22 +14,6 @@ import scala.language.implicitConversions import scala.scalajs.js import scala.scalajs.js.JSConverters._ -/** Factory for virtual DOMs - * - * Virtual DOMs have type of [[Element]], which are implicitly converted to [[ReactElement]]. - * - * Import VirtualDOM._ and access factory methods for DOMs with {{{<}}} and attributes with {{{^}}}. - * {{{ - * import io.github.shogowada.scalajs.reactjs.VirtualDOM._ - * - * object Foo { - * def render(): ReactElement = <.div(^.id := "main")( - * <.div()("first child"), - * <.div()("second child") - * ) - * } - * }}} - * */ object VirtualDOM extends VirtualDOM trait EventVirtualDOMAttributes { @@ -179,18 +165,23 @@ trait EventVirtualDOMAttributes { trait VirtualDOM extends StaticTags { - class VirtualDOMElements extends Elements + class VirtualDOMElements extends Elements { + def apply[Props, State](reactClassSpec: ReactClassSpec[Props, State]): ReactClassElementSpec = + this.apply(React.createClass(reactClassSpec)) + + def apply(reactClass: ReactClass): ReactClassElementSpec = + ReactClassElementSpec(reactClass) + } object VirtualDOMElements { case class ReactClassElementSpec( reactClass: ReactClass ) { def apply(attributes: Any*)(contents: Any*): ReactElement = { - val element = Element("", attributes, contents) React.createElement( reactClass, - VirtualDOMAttributes.toReactAttributes(element.flattenedAttributes), - toReactElements(element.flattenedContents): _* + VirtualDOMAttributes.toReactAttributes(Element.flattenAttributes(attributes)), + toReactElements(Element.flattenContents(contents)): _* ) } @@ -203,7 +194,6 @@ trait VirtualDOM extends StaticTags { private def elementToReactElement(content: Any): js.Any = content match { case element@Element(_, _, _, _) => elementsToVirtualDOMs(element) - case spec: ReactClassSpec[_, _] => React.createElement(spec) case _ => content.asInstanceOf[js.Any] } } @@ -211,20 +201,40 @@ trait VirtualDOM extends StaticTags { class VirtualDOMAttributes extends Attributes with EventVirtualDOMAttributes { - case class RefAttributeSpec(name: String) extends AttributeSpec { - def :=[T <: ReactHTMLElement](callback: js.Function1[T, _]): Attribute[js.Function1[T, _]] = { - Attribute(name, callback, AS_IS) - } - } + import VirtualDOMAttributes._ - lazy val className = SpaceSeparatedStringAttributeSpec(name = "className") + lazy val className = SpaceSeparatedStringAttributeSpec("className") override lazy val `for`: ForAttributeSpec = htmlFor lazy val htmlFor = ForAttributeSpec("htmlFor") lazy val key = StringAttributeSpec("key") lazy val ref = RefAttributeSpec("ref") + lazy val wrapped = WrappedPropsAttributeSpec("wrapped") } object VirtualDOMAttributes { + case class WrappedPropsAttributeSpec(name: String) extends AttributeSpec { + def :=[T](wrappedProps: T): Attribute[js.Any] = + Attribute(name, wrappedProps.asInstanceOf[js.Any], AS_IS) + } + + case class ReactClassAttributeSpec(name: String) extends AttributeSpec { + def :=[Props, State](value: ReactClassSpec[Props, State]): Attribute[ReactClass] = this := React.createClass(value) + def :=(value: ReactClass): Attribute[ReactClass] = Attribute(name, value, AS_IS) + } + + case class RefAttributeSpec(name: String) extends AttributeSpec { + def :=[T <: ReactHTMLElement](callback: js.Function1[T, _]): Attribute[js.Function1[T, _]] = { + Attribute(name, callback, AS_IS) + } + } + + case class RenderAttributeSpec(name: String) extends AttributeSpec { + def :=[WrappedProps](render: Render[WrappedProps]) = { + val nativeRender = ReactClassSpec.renderToNative(render) + Attribute(name, nativeRender, AS_IS) + } + } + object Type { case object AS_IS extends AttributeValueType } @@ -281,7 +291,7 @@ trait VirtualDOM extends StaticTags { private def attributeValueToReactAttributeValue(attribute: Attribute[_]): js.Any = attribute match { case Attribute(_, value, AttributeValueType.CSS) => value.asInstanceOf[Map[String, _]].toJSDictionary - case Attribute(_, value: ReactClassSpec[_, _], AS_IS) => React.createClass(value) + case Attribute(_, value: Boolean, AttributeValueType.DEFAULT) => value.asInstanceOf[js.Any] case Attribute(_, value, AS_IS) => value.asInstanceOf[js.Any] case _ => attribute.valueToString } diff --git a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/classes/specs/ReactClassSpec.scala b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/classes/specs/ReactClassSpec.scala index 228626b..eaa14df 100644 --- a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/classes/specs/ReactClassSpec.scala +++ b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/classes/specs/ReactClassSpec.scala @@ -1,47 +1,20 @@ package io.github.shogowada.scalajs.reactjs.classes.specs -import io.github.shogowada.scalajs.reactjs.React import io.github.shogowada.scalajs.reactjs.elements.ReactElement import io.github.shogowada.scalajs.reactjs.utils.Utils import scala.scalajs.js -/** Specification for React components - * - * Example: - * {{{ - * object Foo { - * case class Props(foo: String) - * case class State(bar: String) - * } - * - * class Foo extends ReactClassSpec[Foo.Props, Foo.State] { - * import Foo._ - * - * override def getInitialState() = State("bar") - * - * override def render(): ReactElement = <.div()( - * s"foo = ${props.foo}", - * s"bar = ${state.bar}", - * children // equivalent of props.children in native React - * ) - * } - * - * val foo = new Foo() - * ReactDOM.render( - * foo(Foo.Props("foo"))( // first parameter group of apply method takes props - * <.div()("first child"), // second parameter group of apply method takes children - * <.div()("second child") - * ), - * mountNode - * ) - * }}} - * */ -trait ReactClassSpec[Props, State] { - - def propsToNative(props: Props) = ReactClassSpec.propsToNative(props) - - def propsFromNative(nativeProps: js.Dynamic) = ReactClassSpec.propsFromNative[Props](nativeProps) +case class Props[Wrapped](native: js.Dynamic) { + def wrapped: Wrapped = native.wrapped.asInstanceOf[Wrapped] + def children: ReactElement = native.children.asInstanceOf[ReactElement] +} + +trait ReactClassSpec[WrappedProps, State] { + + def propsToNative(props: Props[WrappedProps]) = ReactClassSpec.propsToNative(props) + + def propsFromNative(nativeProps: js.Dynamic) = ReactClassSpec.propsFromNative[WrappedProps](nativeProps) def stateToNative(state: State) = ReactClassSpec.stateToNative(state) @@ -51,13 +24,10 @@ trait ReactClassSpec[Props, State] { def nativeState: js.Dynamic = nativeThis.state - def props: Props = propsFromNative(nativeProps) + def props: Props[WrappedProps] = propsFromNative(nativeProps) def state: State = stateFromNative(nativeState) - /** Returns props.children equivalent in native React */ - def children: ReactElement = nativeProps.children.asInstanceOf[ReactElement] - def componentWillMount(): Unit = {} def componentDidMount(): Unit = {} @@ -65,7 +35,7 @@ trait ReactClassSpec[Props, State] { def nativeComponentWillReceiveProps(nativeNextProps: js.Dynamic): Unit = componentWillReceiveProps(propsFromNative(nativeNextProps)) - def componentWillReceiveProps(nextProps: Props): Unit = {} + def componentWillReceiveProps(nextProps: Props[WrappedProps]): Unit = {} def nativeShouldComponentUpdate(nextProps: js.Dynamic, nextState: js.Dynamic): Boolean = { if (shouldComponentUpdate(propsFromNative(nextProps), stateFromNative(nextState))) { @@ -80,18 +50,18 @@ trait ReactClassSpec[Props, State] { } } - def shouldComponentUpdate(nextProps: Props, nextState: State): Boolean = - props != nextProps || state != nextState + def shouldComponentUpdate(nextProps: Props[WrappedProps], nextState: State): Boolean = + props.wrapped != nextProps.wrapped || state != nextState def nativeComponentWillUpdate(nativeNextProps: js.Dynamic, nativeNextState: js.Dynamic): Unit = componentWillUpdate(propsFromNative(nativeNextProps), stateFromNative(nativeNextState)) - def componentWillUpdate(nextProps: Props, nextState: State): Unit = {} + def componentWillUpdate(nextProps: Props[WrappedProps], nextState: State): Unit = {} def nativeComponentDidUpdate(nativePrevProps: js.Dynamic, nativePrevState: js.Dynamic): Unit = componentDidUpdate(propsFromNative(nativePrevProps), stateFromNative(nativePrevState)) - def componentDidUpdate(prevProps: Props, prevState: State): Unit = {} + def componentDidUpdate(prevProps: Props[WrappedProps], prevState: State): Unit = {} def componentWillUnmount(): Unit = {} @@ -108,21 +78,20 @@ trait ReactClassSpec[Props, State] { nativeThis.setState(nativeStateMapper) } - def setState(stateMapper: (State, Props) => State): Unit = { - val nativeStateMapper = (prevState: js.Dynamic, props: js.Dynamic) => stateToNative(stateMapper(stateFromNative(prevState), propsFromNative(props))) + def setState(stateMapper: (State, Props[WrappedProps]) => State): Unit = { + val nativeStateMapper = (prevState: js.Dynamic, props: js.Dynamic) => + stateToNative(stateMapper(stateFromNative(prevState), propsFromNative(props))) nativeThis.setState(nativeStateMapper) } def render(): ReactElement - /** Returns [[ReactElement]] */ - def apply(props: Props)(children: js.Any*): ReactElement = React.createElement(this, props, children: _*) - private var _nativeThis: js.Dynamic = _ def nativeThis: js.Dynamic = _nativeThis def asNative: js.Dynamic = js.Dynamic.literal( + "displayName" -> getClass.getName, "componentWillMount" -> js.ThisFunction.fromFunction1((newNativeThis: js.Dynamic) => { _nativeThis = newNativeThis componentWillMount() @@ -164,12 +133,14 @@ trait ReactClassSpec[Props, State] { object ReactClassSpec { - type Renderer[Props] = Props => ReactElement - type RendererWithChildren[Props] = (Props, ReactElement) => ReactElement + type Render[WrappedProps] = Props[WrappedProps] => ReactElement + + def renderToNative[WrappedProps](render: Render[WrappedProps]): js.Function1[js.Dynamic, ReactElement] = + (nativeProps: js.Dynamic) => render(propsFromNative(nativeProps)) - def propsToNative[Props](props: Props): js.Dynamic = wrap(props) + def propsToNative[WrappedProps](props: Props[WrappedProps]): js.Dynamic = props.native - def propsFromNative[Props](nativeProps: js.Dynamic): Props = unwrap[Props](nativeProps) + def propsFromNative[WrappedProps](nativeProps: js.Dynamic): Props[WrappedProps] = Props(nativeProps) def stateToNative[State](state: State): js.Dynamic = wrap(state) @@ -184,31 +155,12 @@ object ReactClassSpec { nativeWrapped.selectDynamic(WrappedProperty).asInstanceOf[Wrapped] } -/** [[ReactClassSpec]] without state */ trait StatelessReactClassSpec[Props] extends ReactClassSpec[Props, Unit] { override def getInitialState(): Unit = () } -/** [[ReactClassSpec]] without props */ -trait PropslessReactClassSpec[State] extends ReactClassSpec[Unit, State] { - /** Returns [[ReactElement]] - * - * Because [[PropslessReactClassSpec]] does not have props, it only takes one parameter group for children. - * */ - def apply(children: js.Any*): ReactElement = - this.asInstanceOf[ReactClassSpec[Unit, State]] - .apply(())(children: _*) -} +trait PropslessReactClassSpec[State] extends ReactClassSpec[Unit, State] -/** [[ReactClassSpec]] without props and state */ trait StaticReactClassSpec extends ReactClassSpec[Unit, Unit] { override def getInitialState(): Unit = () - - /** Returns [[ReactElement]] - * - * Because [[StaticReactClassSpec]] does not have props, it only takes one parameter group for children. - * */ - def apply(children: js.Any*): ReactElement = - this.asInstanceOf[ReactClassSpec[Unit, Unit]] - .apply(())(children: _*) } diff --git a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/elements/ReactHTMLElements.scala b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/elements/ReactHTMLElements.scala index a721d36..80bc1da 100644 --- a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/elements/ReactHTMLElements.scala +++ b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/elements/ReactHTMLElements.scala @@ -4,36 +4,6 @@ import org.scalajs.dom.raw.HTMLElement import scala.scalajs.js -/** React HTML elements - * - * Get references to those elements by using {{{^.ref}}} attribute. - * {{{ - * object Foo { - * case class State(text: String) - * } - * - * class Foo extends PropslessReactElement[Foo.State] { - * import Foo._ - * - * var inputElement: ReactHTMLInputElement = _ - * - * override def render(): ReactElement = <.div()( - * <.input( - * ^.ref := ((element: ReactHTMLInputElement) => inputElement = element), - * ^.onChange := onChange, - * ^.value := state.text - * )() - * ) - * - * val onChange = () => { - * setState(State(text = inputElement.value)) - * } - * } - * }}} - * - * You can also get references to them via [[io.github.shogowada.scalajs.reactjs.events.FormSyntheticEvent]]. - * */ - @js.native trait ReactHTMLElement extends HTMLElement diff --git a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/events/FormSyntheticEvent.scala b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/events/FormSyntheticEvent.scala index 7be251d..ffa5d22 100644 --- a/core/src/main/scala/io/github/shogowada/scalajs/reactjs/events/FormSyntheticEvent.scala +++ b/core/src/main/scala/io/github/shogowada/scalajs/reactjs/events/FormSyntheticEvent.scala @@ -4,10 +4,8 @@ import io.github.shogowada.scalajs.reactjs.elements._ import scala.scalajs.js -/** [[SyntheticEvent]] for forms */ @js.native trait FormSyntheticEvent[Element <: ReactHTMLElement] extends SyntheticEvent { - /** Reference to corresponding [[ReactHTMLElement]] */ val target: Element = js.native } diff --git a/example/README.md b/example/README.md index 63f260f..7e4195b 100644 --- a/example/README.md +++ b/example/README.md @@ -1,5 +1,116 @@ -# Examples +# Basics -All of those examples are tested by Selenium. You can find Selenium test cases under [test](./test) folder. +## How to replace JSX in Scala? -scalajs-reactjs solely relies on those test cases to test its behaviors. In other words, the library should have example for all the behaviors it provides, and they all should be tested. +To create elements in Scala, import `VirtualDOM._`. `VirtualDOM` is an extended [Static Tags](https://github.com/shogowada/statictags). + +`VirtualDOM` is made of three parts: tag, attributes (a.k.a. props), and children. + +For example, this code + +```scala +<.div(^.id := "hello-world")("Hello, World!") +``` + +is equivalent of the following: + +```html +
Hello, World!
+``` + +You can use as many as attributes and children you want. + +## How to create React components? + +To create React components, extend `ReactClassSpec[WrappedProps, State]` or one of its sub classes. + +You have four options: + +- `ReactClassSpec[WrappedProps, State]` + - This is the parent of them all. You can have both custom props and state. +- `StatelessReactClassSpec[WrappedProps]` + - You cannot have state. +- `PropslessReactClassSpec[State]` + - You cannot have custom props. +- `StaticReactClassSpec` + - You cannot have both custom props and state. + +To render React components, use `<(/* React component */)` to make it an element. You can pass attributes and children like regular elements. + +```scala +import io.github.shogowada.scalajs.reactjs.VirtualDOM._ + +class MyComponent extends ReactClassSpec[WrappedProps, State] { + // ... +} + +ReactDOM.render( + <(new MyComponent())( + // attributes (a.k.a. props) + ^.id := "my-component" + )( + // children + <.div()("I'm your component's child!") + ), + mountNode +) +``` + +### What's WrappedProps? + +While many want to use case classes as props, React requires props to be a plain JavaScript object. So, to use case classes, we need to wrap the case class in another property. In this facade, we wrap it in "wrapped" property. + +```scala +case class WrappedProps(foo: String, bar: Int) + +class MyComponent extends StatelessReactClassSpec[WrappedProps] { + override def render(): ReactElement = + <.div()( + s"foo: ${props.wrapped.foo}", + <.br.empty, + s"bar: ${props.wrapped.bar}" + ) +} + +ReactDOM.render( + <(new MyComponent())(^.wrapped := WrappedProps("foo", 123))(), + mountNode +) +``` + +Props looks like the following: + +```scala +case class Props[Wrapped](native: js.Dynamic) { + def wrapped: Wrapped = native.wrapped.asInstanceOf[Wrapped] + def children: ReactElement = native.children.asInstanceOf[ReactElement] +} +``` + +You can extend it as you see needs. See [`RouterProps`](/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RouterProps.scala) for an example. + +### How about states? + +States are wrapped and unwrapped automatically, so you don't need to do `state.wrapped`. We can wrap and unwrap states automatically because nobody extends states. + +```scala +case class State(text: String) + +class MyComponent extends PropslessReactClassSpec[State] { + override def getInitialState(): State = State(text = "") + + override def render(): ReactElement = + <.div()( + <.input( + ^.placeholder := "Type something here", + ^.value := state.text, + ^.onChange := onChange + )() + ) + + val onChange = (event: InputFormSyntheticEvent) => { + val newText = event.target.value + setState(_.copy(text = newText)) + } +} +``` diff --git a/example/helloworld/src/main/scala/io/github/shogowada/scalajs/reactjs/example/helloworld/Main.scala b/example/helloworld/src/main/scala/io/github/shogowada/scalajs/reactjs/example/helloworld/Main.scala index eb9f810..cda873d 100644 --- a/example/helloworld/src/main/scala/io/github/shogowada/scalajs/reactjs/example/helloworld/Main.scala +++ b/example/helloworld/src/main/scala/io/github/shogowada/scalajs/reactjs/example/helloworld/Main.scala @@ -3,24 +3,23 @@ package io.github.shogowada.scalajs.reactjs.example.helloworld import io.github.shogowada.scalajs.reactjs.ReactDOM import io.github.shogowada.scalajs.reactjs.VirtualDOM._ import io.github.shogowada.scalajs.reactjs.classes.specs.StatelessReactClassSpec -import io.github.shogowada.scalajs.reactjs.elements.ReactElement import org.scalajs.dom import scala.scalajs.js.JSApp object Main extends JSApp { def main(): Unit = { - class HelloWorld extends StatelessReactClassSpec[HelloWorld.Props] { - override def render() = <.div(^.id := "hello-world")(s"Hello, ${props.name}!") + class HelloWorld extends StatelessReactClassSpec[HelloWorld.WrappedProps] { + override def render() = <.div(^.id := "hello-world")(s"Hello, ${props.wrapped.name}!") } object HelloWorld { - case class Props(name: String) + case class WrappedProps(name: String) - def apply(props: Props): ReactElement = (new HelloWorld) (props)() + def apply() = new HelloWorld() } val mountNode = dom.document.getElementById("mount-node") - ReactDOM.render(HelloWorld(HelloWorld.Props("World")), mountNode) + ReactDOM.render(<(HelloWorld())(^.wrapped := HelloWorld.WrappedProps("World"))(), mountNode) } } diff --git a/example/interactive-helloworld/src/main/scala/io/github/shogowada/scalajs/reactjs/example/interactive/helloworld/Main.scala b/example/interactive-helloworld/src/main/scala/io/github/shogowada/scalajs/reactjs/example/interactive/helloworld/Main.scala index 9771f94..3b7998f 100644 --- a/example/interactive-helloworld/src/main/scala/io/github/shogowada/scalajs/reactjs/example/interactive/helloworld/Main.scala +++ b/example/interactive-helloworld/src/main/scala/io/github/shogowada/scalajs/reactjs/example/interactive/helloworld/Main.scala @@ -21,28 +21,28 @@ object LetterCase { } object LetterCaseRadioBox { - case class Props(letterCase: LetterCase, checked: Boolean, onChecked: () => Unit) + case class WrappedProps(letterCase: LetterCase, checked: Boolean, onChecked: () => Unit) - def apply(props: Props): ReactElement = (new LetterCaseRadioBox) (props)() + def apply() = new LetterCaseRadioBox() } -class LetterCaseRadioBox extends StatelessReactClassSpec[LetterCaseRadioBox.Props] { +class LetterCaseRadioBox extends StatelessReactClassSpec[LetterCaseRadioBox.WrappedProps] { override def render(): ReactElement = { <.span()( <.input( ^.`type`.radio, ^.name := "letter-case", - ^.value := props.letterCase.name, - ^.checked := props.checked, + ^.value := props.wrapped.letterCase.name, + ^.checked := props.wrapped.checked, ^.onChange := onChange )(), - props.letterCase.name + props.wrapped.letterCase.name ) } val onChange = (event: RadioFormSyntheticEvent) => { if (event.target.checked) { - props.onChecked() + props.wrapped.onChecked() } } } @@ -81,13 +81,15 @@ class InteractiveHelloWorld extends PropslessReactClassSpec[InteractiveHelloWorl ) def createLetterCaseRadioBox(thisLetterCase: LetterCase): ReactElement = { - LetterCaseRadioBox(LetterCaseRadioBox.Props( - letterCase = thisLetterCase, - checked = thisLetterCase == state.letterCase, - onChecked = () => { - setState(_.copy(letterCase = thisLetterCase)) - } - )) + <(LetterCaseRadioBox())( + ^.wrapped := LetterCaseRadioBox.WrappedProps( + letterCase = thisLetterCase, + checked = thisLetterCase == state.letterCase, + onChecked = () => { + setState(_.copy(letterCase = thisLetterCase)) + } + ) + )() } val onChange = (event: InputFormSyntheticEvent) => { @@ -107,6 +109,6 @@ class InteractiveHelloWorld extends PropslessReactClassSpec[InteractiveHelloWorl object Main extends JSApp { def main(): Unit = { val mountNode = dom.document.getElementById("mount-node") - ReactDOM.render(new InteractiveHelloWorld(), mountNode) + ReactDOM.render(<(new InteractiveHelloWorld()).empty, mountNode) } } diff --git a/example/lifecycle/src/main/scala/io/github/shogowada/scalajs/reactjs/example/lifecycle/Main.scala b/example/lifecycle/src/main/scala/io/github/shogowada/scalajs/reactjs/example/lifecycle/Main.scala index b5822ca..bc0afc5 100644 --- a/example/lifecycle/src/main/scala/io/github/shogowada/scalajs/reactjs/example/lifecycle/Main.scala +++ b/example/lifecycle/src/main/scala/io/github/shogowada/scalajs/reactjs/example/lifecycle/Main.scala @@ -2,7 +2,7 @@ package io.github.shogowada.scalajs.reactjs.example.lifecycle import io.github.shogowada.scalajs.reactjs.ReactDOM import io.github.shogowada.scalajs.reactjs.VirtualDOM._ -import io.github.shogowada.scalajs.reactjs.classes.specs.PropslessReactClassSpec +import io.github.shogowada.scalajs.reactjs.classes.specs.{Props, PropslessReactClassSpec} import io.github.shogowada.scalajs.reactjs.elements.ReactElement import org.scalajs.dom @@ -28,20 +28,20 @@ class App extends PropslessReactClassSpec[App.State] { setState(_.copy(componentDidMountCalled = true)) } - override def shouldComponentUpdate(nextProps: Unit, nextState: State): Boolean = { + override def shouldComponentUpdate(nextProps: Props[Unit], nextState: State): Boolean = { println(s"shouldComponentUpdate($nextProps, $nextState)") nextState != state } - override def componentWillReceiveProps(nextProps: Unit): Unit = { + override def componentWillReceiveProps(nextProps: Props[Unit]): Unit = { println(s"componentWillReceiveProps($nextProps)") } - override def componentWillUpdate(nextProps: Unit, nextState: State): Unit = { + override def componentWillUpdate(nextProps: Props[Unit], nextState: State): Unit = { println(s"componentWillUpdate($nextProps, $nextState)") } - override def componentDidUpdate(prevProps: Unit, prevState: State): Unit = { + override def componentDidUpdate(prevProps: Props[Unit], prevState: State): Unit = { println(s"componentDidUpdate($prevProps, $prevState)") setState(_.copy(componentDidUpdateCalled = true)) } @@ -68,6 +68,6 @@ class App extends PropslessReactClassSpec[App.State] { object Main extends JSApp { override def main(): Unit = { val mountNode = dom.document.getElementById("mount-node") - ReactDOM.render(new App(), mountNode) + ReactDOM.render(<(new App()).empty, mountNode) } } diff --git a/example/routing/src/main/scala/io/github/shogowada/scalajs/reactjs/example/routing/Main.scala b/example/routing/src/main/scala/io/github/shogowada/scalajs/reactjs/example/routing/Main.scala index 6ef1f3f..c149261 100644 --- a/example/routing/src/main/scala/io/github/shogowada/scalajs/reactjs/example/routing/Main.scala +++ b/example/routing/src/main/scala/io/github/shogowada/scalajs/reactjs/example/routing/Main.scala @@ -1,175 +1,150 @@ package io.github.shogowada.scalajs.reactjs.example.routing +import io.github.shogowada.scalajs.reactjs.ReactDOM import io.github.shogowada.scalajs.reactjs.VirtualDOM._ -import io.github.shogowada.scalajs.reactjs.classes.specs.{PropslessReactClassSpec, StaticReactClassSpec} +import io.github.shogowada.scalajs.reactjs.classes.specs.{Props, PropslessReactClassSpec, StaticReactClassSpec} import io.github.shogowada.scalajs.reactjs.elements.ReactElement import io.github.shogowada.scalajs.reactjs.events.CheckBoxFormSyntheticEvent -import io.github.shogowada.scalajs.reactjs.router.Router._ -import io.github.shogowada.scalajs.reactjs.router.{HashHistory, Location, RoutedReactClassSpec, WithRouter} -import io.github.shogowada.scalajs.reactjs.{React, ReactDOM} +import io.github.shogowada.scalajs.reactjs.router.dom.RouterDOM._ +import io.github.shogowada.scalajs.reactjs.router.{RouterProps, WithRouter} import org.scalajs.dom -import scala.scalajs.js import scala.scalajs.js.JSApp +/* + * If you are not yet familiar with react-router, check it our first: + * + * - https://reacttraining.com/react-router/web/guides/quick-start + * + * This is just a facade for the react-router, so if you know how to use it already, + * you will be able to more easily understand how to use this facade. + * + * Import the following to access router components (e.g. Route): + * + * - import io.github.shogowada.scalajs.reactjs.VirtualDOM._ + * - import io.github.shogowada.scalajs.reactjs.router.dom.RouterDOM._ + * */ + object Main extends JSApp { override def main(): Unit = { - /* Import the following to access router components: - * - * - import io.github.shogowada.scalajs.reactjs.VirtualDOM._ - * - import io.github.shogowada.scalajs.reactjs.router.Router._ - * - * */ val mountNode = dom.document.getElementById("mount-node") ReactDOM.render( - <.Router(^.history := HashHistory)( - <.Route(^.path := "/", ^.component := new App())( - <.Route(^.path := "about", ^.component := new About())(), - <.Route(^.path := "repos", ^.component := new Repos())( - <.Route(^.path := ":id", ^.component := new Repo())() - ), - <.Route(^.path := "form", ^.component := new Form())() - ) + <.HashRouter()( + <(App()).empty ), mountNode ) } } +object App { + def apply() = WithRouter(new App()) +} + class App extends StaticReactClassSpec { override def render() = <.div()( <.h1()("React Router Tutorial"), Links(), - RouterApiButtons(), - children + RouterApiButtons(props), + <.Switch()( + <.Route(^.path := "/about", ^.render := (About(_: Props[_])))(), + <.Route(^.path := "/repos", ^.render := (Repos(_: Props[_])))(), + <.Route(^.path := "/form", ^.component := Form())() + ) ).asReactElement } object Links { def apply(): ReactElement = <.nav()( - <.li()(<.Link(^.to := "about")("About")), - <.li()(<.Link(^.to := "repos")("Repos")) + <.li()(<.Link(^.to := "/about")("About")), + <.li()(<.Link(^.to := "/repos")("Repos")) ) } -object RouterApiButtons { - /* Wrap your component with WithRouter if - * - * - You want to use router API - * - The component is not a direct child of the routing components - * - * */ - def apply(): ReactElement = React.createElement(WithRouter(new RouterApiButtons())) -} - -/* Extend RoutedReactClassSpec to access router API. - * If you don't have params, just use js.Object as Params type parameter. - * */ -class RouterApiButtons extends StaticReactClassSpec - with RoutedReactClassSpec[js.Object] { - - override def render(): ReactElement = <.div()( +// Extend RouterProps or import RouterProps._ to access router specific props like props.match or props.history. +object RouterApiButtons extends RouterProps { + def apply(props: Props[_]): ReactElement = <.div()( <.button( ^.id := "push-about", ^.onClick := (() => { - router.push("/about") + props.history.push("/about") }) )("Push /about"), <.button( ^.id := "go-back", ^.onClick := (() => { - router.goBack() + props.history.goBack() }) )("Go back"), <.button( ^.id := "go-forward", ^.onClick := (() => { - router.goForward() + props.history.goForward() }) )("Go forward") ) } -class About extends StaticReactClassSpec { - override def render() = <.div(^.id := "about")("About") +object About { + def apply(props: Props[_]): ReactElement = + <.div(^.id := "about")("About") } -class Repos extends StaticReactClassSpec { - override def render() = <.div(^.id := "repos")( - "Repos", - children - ) +object Repos extends RouterProps { + def apply(props: Props[_]): ReactElement = + <.div(^.id := "repos")( + "Repos", + <.Route( + ^.path := s"${props.`match`.path}/:id", + ^.component := Repo() + )() + ) } object Repo { - @js.native - trait Params extends js.Object { - val id: String = js.native - } + def apply() = new Repo() } class Repo extends StaticReactClassSpec - with RoutedReactClassSpec[Repo.Params] { - override def render() = <.div(^.id := s"repo-${params.id}")(s"Repo ${params.id}") + with RouterProps { + // Params has type of js.Dictionary[String]. + private def id: String = props.`match`.params("id") + + override def render() = <.div(^.id := s"repo-${id}")(s"Repo ${id}") } object Form { case class State(confirmBeforeLeave: Boolean) + + def apply() = new Form() } -class Form extends PropslessReactClassSpec[Form.State] - with RoutedReactClassSpec[js.Object] { +class Form extends PropslessReactClassSpec[Form.State] { import Form._ - private var unsetRouteLeaveHook: () => Unit = _ - - override def getInitialState() = State( - confirmBeforeLeave = true - ) + override def getInitialState() = State(confirmBeforeLeave = true) - override def componentDidMount(): Unit = { - /* Confirm user before leaving the route. - * - * First argument must be route. - * - * Second argument is a hook. The hook can return one of the three: - * - * - true if you want to allow leaving without confirmation - * - false if you want to deny leaving without confirmation - * - confirmation text if you want to confirm user before leaving - * - * It returns a function to unset the hook. - * */ - unsetRouteLeaveHook = router.setRouteLeaveHook(route, (nextLocation: Location) => { - if (state.confirmBeforeLeave) { - "Are you sure you want to leave the page?" - } else { - true - } - }) - } - - override def render(): ReactElement = <.div(^.id := "form")( - <.label()( - "Confirm before leave", - <.input( - ^.id := "confirm-before-leave", - ^.`type`.checkbox, - ^.checked := state.confirmBeforeLeave, - ^.onChange := ((event: CheckBoxFormSyntheticEvent) => { - val checked = event.target.checked - setState(State(confirmBeforeLeave = checked)) - }) - )() - ), - <.button( - ^.id := "unset-route-leave-hook", - ^.onClick := (() => { - unsetRouteLeaveHook() - unsetRouteLeaveHook = null - }) - )("Unset route leave hook") - ) + override def render(): ReactElement = + <.div()( + <.Prompt( + ^.when := state.confirmBeforeLeave, + ^.message := "Are you sure you want to leave the page?" + )(), + <.div(^.id := "form")( + <.label()( + "Confirm before leave", + <.input( + ^.id := "confirm-before-leave", + ^.`type`.checkbox, + ^.checked := state.confirmBeforeLeave, + ^.onChange := ((event: CheckBoxFormSyntheticEvent) => { + val checked = event.target.checked + setState(State(confirmBeforeLeave = checked)) + }) + )() + ) + ) + ) } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/BaseTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/BaseTest.scala new file mode 100644 index 0000000..3570ed5 --- /dev/null +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/BaseTest.scala @@ -0,0 +1,19 @@ +package io.github.shogowada.scalajs.reactjs.example + +import org.openqa.selenium.firefox.FirefoxDriver +import org.scalatest.concurrent.Eventually +import org.scalatest.selenium.{Driver, WebBrowser} +import org.scalatest.{Matchers, path} + +object BaseTest { + val webDriver = new FirefoxDriver() + + Runtime.getRuntime.addShutdownHook(new Thread(() => webDriver.quit())) +} + +trait BaseTest extends path.FreeSpec + with WebBrowser with Driver + with Matchers + with Eventually { + override implicit val webDriver = BaseTest.webDriver +} diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/customvirtualdom/CustomVirtualDOMTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/customvirtualdom/CustomVirtualDOMTest.scala index 32dd3ca..dc330e1 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/customvirtualdom/CustomVirtualDOMTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/customvirtualdom/CustomVirtualDOMTest.scala @@ -1,14 +1,8 @@ package io.github.shogowada.scalajs.reactjs.example.customvirtualdom -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} -class CustomVirtualDOMTest extends path.FreeSpec - with Matchers - with Eventually - with Firefox { +class CustomVirtualDOMTest extends BaseTest { val server = TestTargetServers.customVirtualDOM @@ -19,6 +13,4 @@ class CustomVirtualDOMTest extends path.FreeSpec find(id("hello-world")).map(_.text) should equal(Some("Hello, World!")) } } - - close() } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/helloworld/HelloWorldTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/helloworld/HelloWorldTest.scala index 7f627ef..d3ba1bf 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/helloworld/HelloWorldTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/helloworld/HelloWorldTest.scala @@ -1,26 +1,18 @@ package io.github.shogowada.scalajs.reactjs.example.helloworld -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} -class HelloWorldTest extends path.FunSpec - with Matchers - with Eventually - with Firefox { +class HelloWorldTest extends BaseTest { val server = TestTargetServers.helloWorld - describe("given I am in the homepage") { + "given I am in the homepage" - { go to server.host - it("then it should display Hello World") { + "then it should display Hello World" in { eventually { find("hello-world").get.text should be("Hello, World!") } } } - - close() } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/helloworldfunction/HelloWorldFunctionTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/helloworldfunction/HelloWorldFunctionTest.scala index 12cdcec..4a254a4 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/helloworldfunction/HelloWorldFunctionTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/helloworldfunction/HelloWorldFunctionTest.scala @@ -1,26 +1,18 @@ package io.github.shogowada.scalajs.reactjs.example.helloworldfunction -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} -class HelloWorldFunctionTest extends path.FunSpec - with Matchers - with Eventually - with Firefox { +class HelloWorldFunctionTest extends BaseTest { val server = TestTargetServers.helloWorldFunction - describe("given I am in the homepage") { + "given I am in the homepage" - { go to server.host - it("then it should display Hello World") { + "then it should display Hello World" in { eventually { find("hello-world").get.text should be("Hello, World!") } } } - - close() } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/interactive/helloworld/InteractiveHelloWorldTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/interactive/helloworld/InteractiveHelloWorldTest.scala index 05440eb..dfe56cf 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/interactive/helloworld/InteractiveHelloWorldTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/interactive/helloworld/InteractiveHelloWorldTest.scala @@ -1,59 +1,53 @@ package io.github.shogowada.scalajs.reactjs.example.interactive.helloworld -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} -class InteractiveHelloWorldTest extends path.FunSpec - with Matchers - with Eventually - with Firefox { +class InteractiveHelloWorldTest extends BaseTest { val server = TestTargetServers.interactiveHelloWorld val defaultName = "whoever you are" - describe("given I am in the homepage") { + "given I am in the homepage" - { go to server.host - it("then it should display the default name in input") { + "then it should display the default name in input" in { eventually { textField("name").value should be(defaultName) } } - it("then it should greet the default name") { + "then it should greet the default name" in { eventually { find("greet").get.text should be(s"Hello, $defaultName!") } } - describe("when I changed the name") { + "when I changed the name" - { val newName = "React JS" textField("name").value = newName - it("then it should greet the new name") { + "then it should greet the new name" in { eventually { find("greet").get.text should be(s"Hello, $newName!") } } - describe("when I checked the lower case radio box") { + "when I checked the lower case radio box" - { radioButtonGroup("letter-case").value = "Lower Case" - it("then it should display the name in lower case") { + "then it should display the name in lower case" in { eventually { find("greet").get.text should be(s"Hello, ${newName.toLowerCase}!") } } } - describe("when I checked the upper case radio box") { + "when I checked the upper case radio box" - { radioButtonGroup("letter-case").value = "Upper Case" - it("then it should display the name in upper case") { + "then it should display the name in upper case" in { eventually { find("greet").get.text should be(s"Hello, ${newName.toUpperCase}!") } @@ -61,6 +55,4 @@ class InteractiveHelloWorldTest extends path.FunSpec } } } - - close() } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/lifecycle/LifecycleTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/lifecycle/LifecycleTest.scala index 0f0b8bf..67cfdfc 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/lifecycle/LifecycleTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/lifecycle/LifecycleTest.scala @@ -1,14 +1,8 @@ package io.github.shogowada.scalajs.reactjs.example.lifecycle -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} -class LifecycleTest extends path.FreeSpec - with Firefox - with Eventually - with Matchers { +class LifecycleTest extends BaseTest { val target = TestTargetServers.lifecycle @@ -27,6 +21,4 @@ class LifecycleTest extends path.FreeSpec } } } - - close() } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/routing/RoutingTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/routing/RoutingTest.scala index 727cbd4..60327ed 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/routing/RoutingTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/routing/RoutingTest.scala @@ -1,50 +1,44 @@ package io.github.shogowada.scalajs.reactjs.example.routing -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} import org.openqa.selenium.Alert -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} -class RoutingTest extends path.FunSpec - with Matchers - with Eventually - with Firefox { +class RoutingTest extends BaseTest { val server = TestTargetServers.routing - describe("given I am at home page") { + "given I am at home page" - { go to server.host - it("then it should not display about") { + "then it should not display about" in { find("about") should equal(None) } - it("then it should not display repos") { + "then it should not display repos" in { find("repos") should equal(None) } - describe("when I click on about link") { + "when I click on about link" - { clickOn(linkText("About")) itShouldDisplayAbout() - describe("when I jump to repos via URL") { + "when I jump to repos via URL" - { goToRepos() itShouldDisplayRepos() - describe("when I push /about via history API") { + "when I push /about via history API" - { clickOn(id("push-about")) itShouldDisplayAbout() - describe("when I go back via history API") { + "when I go back via history API" - { clickOn(id("go-back")) itShouldDisplayRepos() - describe("when I go forward via history API") { + "when I go forward via history API" - { clickOn(id("go-forward")) itShouldDisplayAbout() @@ -54,39 +48,39 @@ class RoutingTest extends path.FunSpec } } - describe("when I click on repos link") { + "when I click on repos link" - { clickOn(linkText("Repos")) itShouldDisplayRepos() - describe("when I jump to specific repo") { + "when I jump to specific repo" - { val repoId = 123 goToRepo(repoId) - it("then it should display the repo") { + "then it should display the repo" in { find(s"repo-$repoId").isDefined should equal(true) } } - describe("when I jump to about via URL") { + "when I jump to about via URL" - { goToAbout() itShouldDisplayAbout() } } - describe("when I go to form route") { + "when I go to form route" - { goToForm() itShouldDisplayForm() - describe("and it is to confirm before leave") { + "and it is to confirm before leave" - { confirmBeforeLeave() - describe("when I try to go to about page") { + "when I try to go to about page" - { goToAbout() - it("then it should show confirmation box") { + "then it should show confirmation box" in { eventually { val alert: Alert = webDriver.switchTo().alert() alert.getText should equal("Are you sure you want to leave the page?") @@ -94,13 +88,13 @@ class RoutingTest extends path.FunSpec } } - describe("when I accept the confirmation") { + "when I accept the confirmation" - { webDriver.switchTo().alert().accept() itShouldDisplayAbout() } - describe("when I dismiss the confirmation") { + "when I dismiss the confirmation" - { webDriver.switchTo().alert().dismiss() itShouldDisplayForm() @@ -108,20 +102,10 @@ class RoutingTest extends path.FunSpec } } - describe("and it is not to confirm before leave") { + "and it is not to confirm before leave" - { doNotConfirmBeforeLeave() - describe("when I try to go to about page") { - goToAbout() - - itShouldDisplayAbout() - } - } - - describe("and I unset route leave hook") { - clickOn(id("unset-route-leave-hook")) - - describe("when I try to got to about page") { + "when I try to go to about page" - { goToAbout() itShouldDisplayAbout() @@ -140,7 +124,7 @@ class RoutingTest extends path.FunSpec def itShouldDisplayForm(): Unit = itShouldDisplay("form") def itShouldDisplay(elementId: String): Unit = - it(s"then it should display $elementId") { + s"then it should display $elementId" in { eventually { find(id(elementId)).isDefined should equal(true) } @@ -155,6 +139,4 @@ class RoutingTest extends path.FunSpec clickOn(safeToLeaveCheckBox) } } - - close() } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/style/StyleTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/style/StyleTest.scala index f222ca1..5527147 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/style/StyleTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/style/StyleTest.scala @@ -1,14 +1,8 @@ package io.github.shogowada.scalajs.reactjs.example.style -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} -class StyleTest extends path.FreeSpec - with Matchers - with Eventually - with Firefox { +class StyleTest extends BaseTest { val server = TestTargetServers.style @@ -27,6 +21,4 @@ class StyleTest extends path.FreeSpec find(id("red-text")).flatMap(_.attribute("style")) should equal(Option("color: red;")) } } - - close() } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/TodoAppTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/TodoAppTest.scala index 241f97b..3c7678c 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/TodoAppTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/TodoAppTest.scala @@ -1,65 +1,59 @@ package io.github.shogowada.scalajs.reactjs.example.todoapp -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} -class TodoAppTest extends path.FunSpec - with Matchers - with Eventually - with Firefox { +class TodoAppTest extends BaseTest { val server = TestTargetServers.todoApp - describe("given I am at the page") { + "given I am at the page" - { go to server.host - it("then it should display the header") { + "then it should display the header" in { eventually { find(tagName("h3")).get.text should equal("TODO") } } - it("then it should have no TODO items") { + "then it should have no TODO items" in { eventually { find(tagName("li")) should equal(None) } } - it("then it should display #1 on the button") { + "then it should display #1 on the button" in { eventually { find(tagName("button")).get.text should equal("Add #1") } } - describe("when I add a TODO item") { + "when I add a TODO item" - { val newTodoItem = "new TODO item" addTodoItem(newTodoItem) - it("then it should add the item to the list") { + "then it should add the item to the list" in { eventually { find(tagName("li")).get.text should equal(newTodoItem) } } - it("then it should display #2 on the button") { + "then it should display #2 on the button" in { eventually { find(tagName("button")).get.text should equal("Add #2") } } - it("then it should clear the text") { + "then it should clear the text" in { eventually { textField(tagName("input")).value should equal("") } } - describe("when I add another TODO item") { + "when I add another TODO item" - { val anotherTodoItem = "another TODO item" addTodoItem(anotherTodoItem) - it("then it should add the item to the list") { + "then it should add the item to the list" in { eventually { findAll(tagName("li")).map(_.text).toSeq should equal(Seq( newTodoItem, @@ -75,6 +69,4 @@ class TodoAppTest extends path.FunSpec textField(tagName("input")).value = todoItem submit() } - - close() } diff --git a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/TodoAppReduxTest.scala b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/TodoAppReduxTest.scala index 6b3064a..ae5a0a9 100644 --- a/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/TodoAppReduxTest.scala +++ b/example/test/src/it/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/TodoAppReduxTest.scala @@ -1,14 +1,8 @@ package io.github.shogowada.scalajs.reactjs.example.todoappredux -import io.github.shogowada.scalajs.reactjs.example.TestTargetServers -import org.scalatest.concurrent.Eventually -import org.scalatest.selenium.Firefox -import org.scalatest.{Matchers, path} +import io.github.shogowada.scalajs.reactjs.example.{BaseTest, TestTargetServers} -class TodoAppReduxTest extends path.FreeSpec - with Matchers - with Eventually - with Firefox { +class TodoAppReduxTest extends BaseTest { val server = TestTargetServers.todoAppRedux @@ -34,13 +28,13 @@ class TodoAppReduxTest extends path.FreeSpec completeTodoItem(secondTodoItem) "then it should complete the second todo" in eventually { - findTodoItem(secondTodoItem) - .map(_.underlying.getCssValue("text-decoration")) should equal(Some("line-through")) + findTodoItemOrFail(secondTodoItem) + .underlying.getCssValue("text-decoration") should startWith("line-through") } "but it should not complete the first todo" in eventually { - findTodoItem(firstTodoItem) - .map(_.underlying.getCssValue("text-decoration")) should equal(Some("none")) + findTodoItemOrFail(firstTodoItem) + .underlying.getCssValue("text-decoration") should startWith("none") } @@ -72,9 +66,12 @@ class TodoAppReduxTest extends path.FreeSpec def findTodoItem(text: String): Option[Element] = findAll(tagName("li")).find(_.text == text) + def findTodoItemOrFail(text: String): Element = + findTodoItem(text).getOrElse { + throw new AssertionError(s"Expected todo item '${text}' to be present") + } + def verifyTodoItems(texts: Seq[String]): Unit = eventually { findAll(tagName("li")).map(_.text).toSeq should equal(texts) } - - close() } diff --git a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Actions.scala b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Actions.scala index 3ef00b5..a195fff 100644 --- a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Actions.scala +++ b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Actions.scala @@ -12,7 +12,7 @@ object VisibilityFilters { * You can define actions as case classes. * You don't need to define "type" field; it will automatically use the class name as its "type". * For example, the AddTodo action will have "AddTodo" as a type. -* Make sure your actions extends "Action" trait. +* Make sure actions extends "Action" trait. * */ case class AddTodo(id: Int = AddTodo.nextId, text: String) extends Action diff --git a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/ContainerComponents.scala b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/ContainerComponents.scala index a83772b..d37a52f 100644 --- a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/ContainerComponents.scala +++ b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/ContainerComponents.scala @@ -5,51 +5,41 @@ import io.github.shogowada.scalajs.reactjs.redux.ReactRedux import io.github.shogowada.scalajs.reactjs.redux.Redux.Dispatch /* -* You can create container components by using ReactRedux.connect method. -* The method has three signatures: +* You can create container components by using ReactRedux.connectAdvanced method. * -* - ReactRedux.connect(dispatch, state, ownProps) -* - ReactRedux.connect(dispatch, state) -* - ReactRedux.connect(dispatch) +* You can pass either one of the following to create a component: * -* It shows example for all of them. -* -* Container components are higher-order components. -* This means you need to pass another component to it to create a real component. -* -* You can pass either one of the following to create a real component: -* -* - Render function of type (props: Props, children: ReactElement) => ReactElement -* - Render function of type (props: Props) => ReactElement +* - Render function of type (props: Props[WrappedProps]) => ReactElement * - Presentational component of type ReactClassSpec * -* It shows example for all of them. +* It shows example for both. * */ object ContainerComponents { - case class LinkContainerComponentProps(filter: String) + case class LinkContainerComponentOwnProps(filter: String) implicit class RichVirtualDOMElements(virtualDOMElements: VirtualDOMElements) { def LinkContainerComponent = ReactRedux.connectAdvanced( (dispatch: Dispatch) => { - var ownProps: LinkContainerComponentProps = null + var ownProps: LinkContainerComponentOwnProps = null val onClick: () => Unit = () => dispatch(SetVisibilityFilter(filter = ownProps.filter)) - (state: State, nextOwnProps: LinkContainerComponentProps) => { + + (state: State, nextOwnProps: LinkContainerComponentOwnProps) => { ownProps = nextOwnProps - Link.Props( + Link.WrappedProps( active = ownProps.filter == state.visibilityFilter, onClick = onClick ) } } - )(Link(_, _)) // (Props, ReactElement) => ReactElement + )(Link(_)) // (Props[WrappedProps]) => ReactElement def TodoListContainerComponent = ReactRedux.connectAdvanced( (dispatch: Dispatch) => { val onTodoClick: (Int) => Unit = (id: Int) => dispatch(ToggleTodo(id = id)) (state: State, ownProps: Unit) => { - TodoList.Props( + TodoList.WrappedProps( todos = state.visibilityFilter match { case VisibilityFilters.ShowAll => state.todos case VisibilityFilters.ShowActive => state.todos.filter(todo => !todo.completed) @@ -59,13 +49,13 @@ object ContainerComponents { ) } } - )(TodoList(_)) // (Props) => ReactElement + )(TodoList(_)) // (Props[WrappedProps]) => ReactElement def AddTodoContainerComponent = ReactRedux.connectAdvanced( (dispatch: Dispatch) => { val onAddTodo: (String) => Unit = (text: String) => dispatch(AddTodo(text = text)) (state: State, ownProps: Unit) => - AddTodoComponent.Props( + AddTodoComponent.WrappedProps( onAddTodo = onAddTodo ) } diff --git a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Main.scala b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Main.scala index 5db65f6..7f048f6 100644 --- a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Main.scala +++ b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Main.scala @@ -8,22 +8,26 @@ import org.scalajs.dom import scala.scalajs.js.JSApp +/* + * If you are not familiar with react-redux yet, please check it out first: + * + * - http://redux.js.org/docs/basics/UsageWithReact.html + * */ + object Main extends JSApp { override def main(): Unit = { val mountNode = dom.document.getElementById("mount-node") - // Use Redux.createStore(reducer) to create a store. val store = Redux.createStore(Reducer.reduce) /* - * Use <.Provider(store)(children) to create a virtual DOM for your Redux containers. - * Note that you need to import the following to access the Provider: - * - * - import io.github.shogowada.scalajs.reactjs.VirtualDOM._ - * - import io.github.shogowada.scalajs.reactjs.redux.ReactRedux._ - * */ + * Import the following to access the Provider: + * + * - import io.github.shogowada.scalajs.reactjs.VirtualDOM._ + * - import io.github.shogowada.scalajs.reactjs.redux.ReactRedux._ + * */ ReactDOM.render( - <.Provider(store = store)( + <.Provider(^.store := store)( App() ), mountNode diff --git a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/PresentationalComponents.scala b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/PresentationalComponents.scala index 41ed198..55cf385 100644 --- a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/PresentationalComponents.scala +++ b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/PresentationalComponents.scala @@ -1,7 +1,7 @@ package io.github.shogowada.scalajs.reactjs.example.todoappredux import io.github.shogowada.scalajs.reactjs.VirtualDOM._ -import io.github.shogowada.scalajs.reactjs.classes.specs.StatelessReactClassSpec +import io.github.shogowada.scalajs.reactjs.classes.specs.{Props, StatelessReactClassSpec} import io.github.shogowada.scalajs.reactjs.elements.{ReactElement, ReactHTMLInputElement} import io.github.shogowada.scalajs.reactjs.events.{InputFormSyntheticEvent, SyntheticEvent} import io.github.shogowada.scalajs.reactjs.example.todoappredux.ContainerComponents._ @@ -20,34 +20,34 @@ object Todo { } object TodoList { - case class Props(todos: Seq[TodoItem], onTodoClick: (Int) => Unit) + case class WrappedProps(todos: Seq[TodoItem], onTodoClick: (Int) => Unit) - def apply(props: Props): ReactElement = + def apply(props: Props[WrappedProps]): ReactElement = <.ul()( - props.todos.map(todo => { + props.wrapped.todos.map(todo => { Todo(Todo.Props( todoItem = todo, - onClick = () => props.onTodoClick(todo.id) + onClick = () => props.wrapped.onTodoClick(todo.id) )) }) ) } object Link { - case class Props(active: Boolean, onClick: () => Unit) + case class WrappedProps(active: Boolean, onClick: () => Unit) - def apply(props: Props, children: ReactElement): ReactElement = - if (props.active) { - <.span()(children) + def apply(props: Props[WrappedProps]): ReactElement = + if (props.wrapped.active) { + <.span()(props.children) } else { <.a( ^.href := "#", ^.onClick := ((e: SyntheticEvent) => { e.preventDefault() - props.onClick() + props.wrapped.onClick() }) )( - children + props.children ) } } @@ -56,21 +56,28 @@ object Footer { def apply(): ReactElement = <.p()( "Show: ", - <.LinkContainerComponent(LinkContainerComponentProps("SHOW_ALL"))( + <.LinkContainerComponent( + // Make sure to wrap own props with "wrapped" property + ^.wrapped := LinkContainerComponentOwnProps("SHOW_ALL") + )( "All" ), ", ", - <.LinkContainerComponent(LinkContainerComponentProps("SHOW_ACTIVE"))( + <.LinkContainerComponent( + ^.wrapped := LinkContainerComponentOwnProps("SHOW_ACTIVE") + )( "Active" ), ", ", - <.LinkContainerComponent(LinkContainerComponentProps("SHOW_COMPLETED"))( + <.LinkContainerComponent( + ^.wrapped := LinkContainerComponentOwnProps("SHOW_COMPLETED") + )( "Completed" ) ) } -class AddTodoComponent extends StatelessReactClassSpec[AddTodoComponent.Props] { +class AddTodoComponent extends StatelessReactClassSpec[AddTodoComponent.WrappedProps] { private var input: ReactHTMLInputElement = _ @@ -80,7 +87,7 @@ class AddTodoComponent extends StatelessReactClassSpec[AddTodoComponent.Props] { ^.onSubmit := ((event: InputFormSyntheticEvent) => { event.preventDefault() if (!input.value.trim.isEmpty) { - props.onAddTodo(input.value) + props.wrapped.onAddTodo(input.value) input.value = "" } }) @@ -94,14 +101,14 @@ class AddTodoComponent extends StatelessReactClassSpec[AddTodoComponent.Props] { } object AddTodoComponent { - case class Props(onAddTodo: (String) => Unit) + case class WrappedProps(onAddTodo: (String) => Unit) } object App { def apply(): ReactElement = <.div()( - <.AddTodoContainerComponent(), - <.TodoListContainerComponent(), + <.AddTodoContainerComponent.empty, + <.TodoListContainerComponent.empty, Footer() ) } diff --git a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Reducer.scala b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Reducer.scala index 6d2f305..84e82e5 100644 --- a/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Reducer.scala +++ b/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux/Reducer.scala @@ -4,7 +4,7 @@ import io.github.shogowada.scalajs.reactjs.redux.Action /* * Reducer function has a signature of (Option[State], Action) => State. -* If the state is absent, you need to return an initial state. +* If the state is absent, return an initial state. * */ object Reducer { def reduce(maybeState: Option[State], action: Action): State = @@ -13,32 +13,30 @@ object Reducer { visibilityFilter = reduceVisibilityFilter(maybeState.map(_.visibilityFilter), action) ) - private def reduceTodos(maybeTodos: Option[Seq[TodoItem]], action: Action): Seq[TodoItem] = - maybeTodos.map(todos => - action match { - case action: AddTodo => { - todos :+ TodoItem( - id = action.id, - text = action.text, - completed = false - ) - } - case action: ToggleTodo => { - todos.map(todo => if (todo.id == action.id) { - todo.copy(completed = !todo.completed) - } else { - todo - }) - } - case _ => todos + private def reduceTodos(maybeTodos: Option[Seq[TodoItem]], action: Action): Seq[TodoItem] = { + val todos = maybeTodos.getOrElse(Seq.empty) + action match { + case action: AddTodo => { + todos :+ TodoItem( + id = action.id, + text = action.text, + completed = false + ) } - ).getOrElse(Seq()) + case action: ToggleTodo => { + todos.map(todo => if (todo.id == action.id) { + todo.copy(completed = !todo.completed) + } else { + todo + }) + } + case _ => todos + } + } private def reduceVisibilityFilter(maybeVisibilityFilter: Option[String], action: Action): String = - maybeVisibilityFilter.map(visibilityFilter => - action match { - case action: SetVisibilityFilter => action.filter - case _ => visibilityFilter - } - ).getOrElse(VisibilityFilters.ShowAll) + action match { + case action: SetVisibilityFilter => action.filter + case _ => maybeVisibilityFilter.getOrElse(VisibilityFilters.ShowAll) + } } diff --git a/example/todo-app/README.md b/example/todo-app/README.md deleted file mode 100644 index 7eabb5a..0000000 --- a/example/todo-app/README.md +++ /dev/null @@ -1,246 +0,0 @@ -# TODO App Example - -In this example, we will replicate [the TODO App application from the official React website](https://facebook.github.io/react/). - -- [JSX vs. Scala](#jsx-vs-scala) - - [JSX](#jsx) - - [Scala](#scala) -- [Define a new React Component](#define-a-new-react-component) -- [Define props and state](#define-props-and-state) -- [Define an initial state](#define-an-initial-state) -- [Define render method](#define-render-method) -- [Update state](#update-state) -- [Mount it to DOM](#mount-it-to-dom) - -## JSX vs. Scala - -Before going into the details, let's see how each of them (JSX and Scala) look like. - -### JSX - -```jsx -class TodoApp extends React.Component { - constructor(props) { - super(props); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.state = {items: [], text: ''}; - } - - render() { - return ( -
-

TODO

- -
- - -
-
- ); - } - - handleChange(e) { - this.setState({text: e.target.value}); - } - - handleSubmit(e) { - e.preventDefault(); - var newItem = { - text: this.state.text, - id: Date.now() - }; - this.setState((prevState) => ({ - items: prevState.items.concat(newItem), - text: '' - })); - } -} - -class TodoList extends React.Component { - render() { - return ( - - ); - } -} - -ReactDOM.render(, mountNode); -``` - -### Scala - -```scala -case class Item(id: String, text: String) - -object TodoApp { - case class State(items: Seq[Item], text: String) - - def apply() = new TodoApp() -} - -class TodoApp extends PropslessReactClassSpec[TodoApp.State] { - import TodoApp._ - - override def getInitialState() = State(items = Seq(), text = "") - - override def render(): ReactElement = { - <.div()( - <.h3()("TODO"), - TodoList(TodoList.Props(items = state.items)), - <.form(^.onSubmit := handleSubmit)( - <.input(^.onChange := handleChange, ^.value := state.text)(), - <.button()(s"Add #${state.items.size + 1}") - ) - ) - } - - private val handleChange = (e: InputFormSyntheticEvent) => { - val newText = e.target.value - setState(_.copy(text = newText)) - } - - private val handleSubmit = (e: SyntheticEvent) => { - e.preventDefault() - val newItem = Item(text = state.text, id = js.Date.now().toString) - setState((prevState: State) => State( - items = prevState.items :+ newItem, - text = "" - )) - } -} - -object TodoList { - case class Props(items: Seq[Item]) - - def apply(props: Props): ReactElement = <.ul()(props.items.map(item => <.li(^.key := item.id)(item.text))) -} - -val mountNode = dom.document.getElementById("mount-node") -ReactDOM.render(TodoApp(), mountNode) -``` - -## Define a new React Component - -To define a new React Component, you need to create a new class extending ```ReactClassSpec``` or its subclasses. - -```scala -object TodoApp { - case class State(items: Seq[Item], text: String) -} - -class TodoApp extends PropslessReactClassSpec[TodoApp.State] - -object TodoList { - case class Props(items: Seq[Item]) -} -```` - -## Define an initial state - -If your class spec is stateful, you need to give it an initial state too. You don't need to do this for ```StatelessReactClassSpec```. - -```scala -class TodoApp extends PropslessReactClassSpec[TodoApp.State] { - import TodoApp._ - - override def getInitialState() = State(items = Seq(), text = "") -} -``` - -## Define render method - -You can render your virual DOMs using ```VirtualDOM``` class. To use it, ```import io.github.shogowada.scalajs.reactjs.VirtualDOM._``` first, then start writing tags with ```<``` and attributes with ```^```. - -```scala -import io.github.shogowada.scalajs.reactjs.VirtualDOM._ - -class TodoApp extends PropslessReactClassSpec[TodoApp.State] { - override def render(): ReactElement = { - <.div()( - <.h3()("TODO"), - TodoList(TodoList.Props(items = state.items)), - <.form(^.onSubmit := handleSubmit)( - <.input(^.onChange := handleChange, ^.value := state.text)(), - <.button()(s"Add #${state.items.size + 1}") - ) - ) - } -} - -object TodoList { - case class Props(items: Seq[Item]) - - def apply(props: Props): ReactElement = <.ul()(props.items.map(item => <.li(^.key := item.id)(item.text))) -} -``` - -In this example, TodoApp is implementing a render method by extending `ReactClassSpec`, and TodoList is implementing a render method as a pure function. - -## Update state - -If your component is stateful, you can update the state by using ```setState``` method. - -```scala - -class TodoApp extends PropslessReactClassSpec[TodoApp.State] { - import TodoApp._ - - private val handleChange = (e: InputFormSyntheticEvent) => { - val newText = e.target.value - setState(_.copy(text = newText)) - } - - private val handleSubmit = (e: SyntheticEvent) => { - e.preventDefault() - val newItem = Item(text = state.text, id = js.Date.now().toString) - setState((prevState: State) => State( - items = prevState.items :+ newItem, - text = "" - )) - } -} -``` - -Unless you are overriding the whole state, you need to copy the previous state to generate a new state. This is because state update is async process, and you cannot partially update the state unlike JavaScript. - -For example, the following might cause race condition and override ```items``` value with old one unexpectedly. - -```scala -setState(state.copy(text = e.target.value)) -``` - -So it needs to be the following instead: - -```scala -val newText = e.target.value -setState((prevState: State) => prevState.copy(text = newText)) -``` - -Or, to make it less verbose: - -```scala -val newText = e.target.value -setState(_.copy(text = newText)) -``` - -If the new state depends on the current state, you can use the previous state to generate the new state. - -```scala -setState((prevState: State) => State( - items = prevState.items :+ newItem, - text = "" -)) -``` - -## Mount it to DOM - -Finally, you can mount your React component to DOM. - -```scala -ReactDOM.render(TodoApp(), mountNode) -``` diff --git a/example/todo-app/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/Main.scala b/example/todo-app/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/Main.scala index af8ae2b..2c40c29 100644 --- a/example/todo-app/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/Main.scala +++ b/example/todo-app/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoapp/Main.scala @@ -12,53 +12,57 @@ import scala.scalajs.js.JSApp object Main extends JSApp { def main(): Unit = { - case class Item(id: String, text: String) - - object TodoApp { - case class State(items: Seq[Item], text: String) - - def apply() = new TodoApp() - } + val mountNode = dom.document.getElementById("mount-node") + ReactDOM.render( + <(TodoApp()).empty, + mountNode + ) + } +} - class TodoApp extends PropslessReactClassSpec[TodoApp.State] { +case class Item(id: String, text: String) - import TodoApp._ +object TodoApp { + case class State(items: Seq[Item], text: String) - override def getInitialState() = State(items = Seq(), text = "") + def apply() = new TodoApp() +} - override def render(): ReactElement = { - <.div()( - <.h3()("TODO"), - TodoList(TodoList.Props(items = state.items)), - <.form(^.onSubmit := handleSubmit)( - <.input(^.onChange := handleChange, ^.value := state.text)(), - <.button()(s"Add #${state.items.size + 1}") - ) - ) - } +class TodoApp extends PropslessReactClassSpec[TodoApp.State] { - private val handleChange = (e: InputFormSyntheticEvent) => { - val newText = e.target.value - setState(_.copy(text = newText)) - } + import TodoApp._ - private val handleSubmit = (e: SyntheticEvent) => { - e.preventDefault() - val newItem = Item(text = state.text, id = js.Date.now().toString) - setState((prevState: State) => State( - items = prevState.items :+ newItem, - text = "" - )) - } - } + override def getInitialState() = State(items = Seq.empty, text = "") - object TodoList { - case class Props(items: Seq[Item]) + override def render(): ReactElement = + <.div()( + <.h3()("TODO"), + TodoList(state.items), + <.form(^.onSubmit := handleSubmit)( + <.input(^.onChange := handleChange, ^.value := state.text)(), + <.button()(s"Add #${state.items.size + 1}") + ) + ) - def apply(props: Props): ReactElement = <.ul()(props.items.map(item => <.li(^.key := item.id)(item.text))) - } + private val handleChange = (e: InputFormSyntheticEvent) => { + // Cache the value because React reuses the event object. + val newText = e.target.value + // It is a syntactic sugar for setState((prevState: State) => prevState.copy(text = newText)) + setState(_.copy(text = newText)) + } - val mountNode = dom.document.getElementById("mount-node") - ReactDOM.render(TodoApp(), mountNode) + private val handleSubmit = (e: SyntheticEvent) => { + e.preventDefault() + val newItem = Item(text = state.text, id = js.Date.now().toString) + setState((prevState: State) => State( + items = prevState.items :+ newItem, + text = "" + )) } } + +object TodoList { + // Use a pure function to render + def apply(items: Seq[Item]): ReactElement = + <.ul()(items.map(item => <.li(^.key := item.id)(item.text))) +} diff --git a/redux/README.md b/redux/README.md index 075ce8d..607315d 100644 --- a/redux/README.md +++ b/redux/README.md @@ -1,12 +1,3 @@ # scalajs-reactjs-redux -A facade for [react-redux](https://github.com/reactjs/react-redux). - -## Examples - -[Click here for an example](/example/todo-app-redux/src/main/scala/io/github/shogowada/scalajs/reactjs/example/todoappredux). The example is trying to replicate [the official react-redux tutorial](http://redux.js.org/docs/basics/UsageWithReact.html). - -## API References - -- [`Redux`](./src/main/scala/io/github/shogowada/scalajs/reactjs/redux/Redux.scala) -- [`ReactRedux`](./src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ReactRedux.scala) +A facade for [react-redux](https://github.com/reactjs/react-redux) diff --git a/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ContainerComponent.scala b/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ContainerComponent.scala new file mode 100644 index 0000000..30f9940 --- /dev/null +++ b/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ContainerComponent.scala @@ -0,0 +1,47 @@ +package io.github.shogowada.scalajs.reactjs.redux + +import io.github.shogowada.scalajs.reactjs.React +import io.github.shogowada.scalajs.reactjs.VirtualDOM.{VirtualDOMAttributes, VirtualDOMElements} +import io.github.shogowada.scalajs.reactjs.classes.ReactClass +import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec +import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec.Render +import io.github.shogowada.scalajs.reactjs.elements.ReactElement +import io.github.shogowada.statictags.Element + +import scala.scalajs.js + +object ContainerComponent { + def ownPropsFromNative[OwnProps](nativeOwnProps: js.Dynamic): OwnProps = + nativeOwnProps.wrapped.asInstanceOf[OwnProps] + + def ownPropsToNative[OwnProps](ownProps: OwnProps): js.Dynamic = + js.Dynamic.literal("wrapped" -> ownProps.asInstanceOf[js.Any]) +} + +class ContainerComponent(containerComponent: ReactClass) { + def apply(attributes: Any*)(children: Any*): ReactElement = { + React.createElement( + containerComponent, + VirtualDOMAttributes.toReactAttributes(Element.flattenAttributes(attributes)), + VirtualDOMElements.toReactElements(Element.flattenContents(children)): _* + ) + } + + def empty = this.apply()() +} + +class ContainerComponentFactory[WrappedProps](nativeFactory: js.Function1[js.Any, ReactClass]) { + def apply[State](classSpec: ReactClassSpec[WrappedProps, State]): ContainerComponent = + this.apply(React.createClass(classSpec)) + + def apply[State](reactClass: ReactClass): ContainerComponent = { + val nativeContainerComponent: ReactClass = nativeFactory(reactClass) + new ContainerComponent(nativeContainerComponent) + } + + def apply(render: Render[WrappedProps]): ContainerComponent = { + val nativeRender = ReactClassSpec.renderToNative(render) + val nativeContainerComponent: ReactClass = nativeFactory(nativeRender) + new ContainerComponent(nativeContainerComponent) + } +} diff --git a/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ReactRedux.scala b/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ReactRedux.scala index 1db624d..6db1d5a 100644 --- a/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ReactRedux.scala +++ b/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/ReactRedux.scala @@ -1,111 +1,46 @@ package io.github.shogowada.scalajs.reactjs.redux -import io.github.shogowada.scalajs.reactjs.React -import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMElements +import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMAttributes.Type.AS_IS +import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMElements.ReactClassElementSpec +import io.github.shogowada.scalajs.reactjs.VirtualDOM.{VirtualDOMAttributes, VirtualDOMElements} import io.github.shogowada.scalajs.reactjs.classes.ReactClass import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec -import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec.{Renderer, RendererWithChildren} -import io.github.shogowada.scalajs.reactjs.elements.ReactElement +import io.github.shogowada.scalajs.reactjs.redux.ReactRedux.ReactReduxVirtualDOMAttributes.StoreAttributeSpec import io.github.shogowada.scalajs.reactjs.redux.Redux.NativeDispatch +import io.github.shogowada.statictags.{Attribute, AttributeSpec} import scala.scalajs.js import scala.scalajs.js.annotation.JSImport -object ContainerComponent { - def ownPropsFromNative[OwnProps](nativeOwnProps: js.Dynamic): OwnProps = nativeOwnProps.wrapped.asInstanceOf[OwnProps] - - def ownPropsToNative[OwnProps](ownProps: OwnProps): js.Dynamic = js.Dynamic.literal( - "wrapped" -> ownProps.asInstanceOf[js.Any] - ) -} - -class ContainerComponent[OwnProps](wrappedClass: ReactClass) { - def apply(children: js.Any*): ReactElement = { - React.createElement(wrappedClass, (), children: _*) - } - - def apply(ownProps: OwnProps)(children: js.Any*): ReactElement = { - val nativeOwnProps = ContainerComponent.ownPropsToNative(ownProps) - React.createElement(wrappedClass, nativeOwnProps, children: _*) - } -} - -class ContainerComponentFactory[OwnProps, Props](nativeFactory: js.Function1[js.Any, ReactClass]) { - def apply[State](classSpec: ReactClassSpec[Props, State]): ContainerComponent[OwnProps] = - this.apply(React.createClass(classSpec)) - - def apply[State](reactClass: ReactClass): ContainerComponent[OwnProps] = { - val nativeContainerComponent: ReactClass = nativeFactory(reactClass) - new ContainerComponent[OwnProps](nativeContainerComponent) - } - - def apply(renderer: Renderer[Props]): ContainerComponent[OwnProps] = { - apply((props: Props, children: ReactElement) => renderer(props)) - } - - def apply(renderer: RendererWithChildren[Props]): ContainerComponent[OwnProps] = { - val nativeRenderer: js.Function1[js.Dynamic, ReactElement] = (nativeProps: js.Dynamic) => { - val props = ReactClassSpec.propsFromNative[Props](nativeProps) - val nativeChildren: js.Dynamic = nativeProps.children - val children: ReactElement = if (js.isUndefined(nativeChildren)) { - null - } else { - nativeChildren.asInstanceOf[ReactElement] - } - renderer(props, children) - } - val nativeContainerComponent: ReactClass = nativeFactory(nativeRenderer) - new ContainerComponent[OwnProps](nativeContainerComponent) - } -} - -/** Facade for react-redux */ object ReactRedux { import Redux.Dispatch - /** [[io.github.shogowada.scalajs.reactjs.VirtualDOM]] extension for react-redux components */ - implicit class RichVirtualDOMElements(elements: VirtualDOMElements) { - def Provider(store: Store)(child: ReactElement): ReactElement = { - val props = js.Dynamic.literal( - "store" -> store - ) - React.createElement(NativeReactReduxProvider, props, child) - } - } - - def connect[Props]( - selector: (Dispatch) => Props - ): ContainerComponentFactory[Unit, Props] = { - connect((dispatch: Dispatch, _: js.Any) => selector(dispatch)) + implicit class ReactReduxVirtualDOMElements(elements: VirtualDOMElements) { + lazy val Provider = ReactClassElementSpec(NativeReactReduxProvider) } - def connect[ReduxState, Props, State]( - selector: (Dispatch, ReduxState) => Props - ): ContainerComponentFactory[Unit, Props] = { - connect((dispatch: Dispatch, state: ReduxState, ownProps: Unit) => selector(dispatch, state)) + object ReactReduxVirtualDOMAttributes { + case class StoreAttributeSpec(name: String) extends AttributeSpec { + def :=(store: Store) = Attribute(name, store, AS_IS) + } } - def connect[ReduxState, OwnProps, Props, State]( - selector: (Dispatch, ReduxState, OwnProps) => Props - ): ContainerComponentFactory[OwnProps, Props] = { - connectAdvanced( - (dispatch: Dispatch) => (state: ReduxState, ownProps: OwnProps) => - selector(dispatch, state, ownProps) - ) + implicit class ReactReduxVirtualDOMAttributes(attributes: VirtualDOMAttributes) { + lazy val store = StoreAttributeSpec("store") } - def connectAdvanced[ReduxState, OwnProps, Props]( - selectorFactory: Dispatch => (ReduxState, OwnProps) => Props - ): ContainerComponentFactory[OwnProps, Props] = { + def connectAdvanced[ReduxState, OwnProps, WrappedProps]( + selectorFactory: Dispatch => (ReduxState, OwnProps) => WrappedProps + ): ContainerComponentFactory[WrappedProps] = { val nativeSelectorFactory = selectorFactoryToNative(selectorFactory) val nativeFactory = NativeReactRedux.connectAdvanced(nativeSelectorFactory) - new ContainerComponentFactory[OwnProps, Props](nativeFactory) + new ContainerComponentFactory[WrappedProps](nativeFactory) } - private def selectorFactoryToNative[ReduxState, OwnProps, Props]( - selectorFactory: Dispatch => (ReduxState, OwnProps) => Props + private def selectorFactoryToNative[ReduxState, OwnProps, WrappedProps]( + selectorFactory: Dispatch => (ReduxState, OwnProps) => WrappedProps ): js.Function1[NativeDispatch, js.Function2[ReduxState, js.Dynamic, js.Any]] = (nativeDispatch: NativeDispatch) => { val dispatch: Dispatch = dispatchFromNative(nativeDispatch) @@ -113,31 +48,29 @@ object ReactRedux { selectorToNative(selector) } - private def selectorToNative[ReduxState, OwnProps, Props]( - selector: (ReduxState, OwnProps) => Props + private def selectorToNative[ReduxState, OwnProps, WrappedProps]( + selector: (ReduxState, OwnProps) => WrappedProps ): js.Function2[ReduxState, js.Dynamic, js.Any] = (state: ReduxState, nativeOwnProps: js.Dynamic) => { val ownProps: OwnProps = ContainerComponent.ownPropsFromNative(nativeOwnProps) - val props: Props = selector(state, ownProps) - propsToNative(props, nativeOwnProps) + val wrappedProps: WrappedProps = selector(state, ownProps) + val nativeProps = clone(nativeOwnProps) + nativeProps.updateDynamic(ReactClassSpec.WrappedProperty)(wrappedProps.asInstanceOf[js.Any]) + nativeProps } + private def clone(plainObject: js.Dynamic): js.Dynamic = { + val clonedPlainObject = js.Dynamic.literal() + val keys = js.Object.keys(plainObject.asInstanceOf[js.Object]) + keys.foreach(key => clonedPlainObject.updateDynamic(key)(plainObject.selectDynamic(key))) + clonedPlainObject + } + private def dispatchFromNative(nativeDispatch: NativeDispatch): Dispatch = (action: Action) => { val nativeAction = Action.actionToNative(action) Action.actionFromNative(nativeDispatch(nativeAction)) } - - private def propsToNative[Props]( - props: Props, - nativeOwnProps: js.Dynamic - ): js.Any = { - val nativeProps = ReactClassSpec.propsToNative(props) - nativeProps.children = nativeOwnProps.children - nativeProps.params = nativeOwnProps.params - nativeProps - } - } @js.native diff --git a/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/Redux.scala b/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/Redux.scala index 7986a12..e727138 100644 --- a/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/Redux.scala +++ b/redux/src/main/scala/io/github/shogowada/scalajs/reactjs/redux/Redux.scala @@ -17,7 +17,6 @@ trait Action @js.native trait Store extends js.Object -/** Facade for redux */ object Redux { type NativeDispatch = js.Function1[js.Dynamic, js.Dynamic] type Dispatch = Action => Action diff --git a/router-dom/src/main/scala/io/github/shogowada/scalajs/reactjs/router/dom/RouterDOM.scala b/router-dom/src/main/scala/io/github/shogowada/scalajs/reactjs/router/dom/RouterDOM.scala new file mode 100644 index 0000000..71f47bb --- /dev/null +++ b/router-dom/src/main/scala/io/github/shogowada/scalajs/reactjs/router/dom/RouterDOM.scala @@ -0,0 +1,75 @@ +package io.github.shogowada.scalajs.reactjs.router.dom + +import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMAttributes.Type.AS_IS +import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMElements.ReactClassElementSpec +import io.github.shogowada.scalajs.reactjs.VirtualDOM.{VirtualDOMAttributes, VirtualDOMElements} +import io.github.shogowada.scalajs.reactjs.classes.ReactClass +import io.github.shogowada.scalajs.reactjs.router +import io.github.shogowada.scalajs.reactjs.router.{Location, Match} +import io.github.shogowada.statictags.{AttributeSpec, _} + +import scala.scalajs.js +import scala.scalajs.js.annotation.JSImport + +@js.native +@JSImport("react-router-dom", "BrowserRouter") +object NativeBrowserRouter extends ReactClass + +@js.native +@JSImport("react-router-dom", "HashRouter") +object NativeHashRouter extends ReactClass + +@js.native +@JSImport("react-router-dom", "Link") +object NativeLink extends ReactClass + +@js.native +@JSImport("react-router-dom", "NavLink") +object NativeNavLink extends ReactClass + +trait RouterDOM extends router.Router { + implicit class RouterDOMVirtualDOMElements(elements: VirtualDOMElements) { + lazy val BrowserRouter = ReactClassElementSpec(NativeBrowserRouter) + lazy val HashRouter = ReactClassElementSpec(NativeHashRouter) + lazy val Link = ReactClassElementSpec(NativeLink) + lazy val NavLink = ReactClassElementSpec(NativeNavLink) + } + + object RouterDOMVirtualDOMAttributes { + case class GetUserConfirmationAttributeSpec(name: String) extends AttributeSpec { + type Get = js.Function2[String, js.Function1[Boolean, _], _] + def :=(get: Get): Attribute[Get] = Attribute(name, get, AS_IS) + } + + case class HashTypeAttributeSpec(name: String) extends AttributeSpec { + def :=(hashType: String) = Attribute(name, hashType) + + lazy val hashbang = this := ("hashbang") + lazy val noslash = this := ("noslash") + lazy val slash = this := ("slash") + } + + case class IsActiveAttributeSpec(name: String) extends AttributeSpec { + type IsActive = js.Function2[Match, Location, Boolean] + def :=(isActive: IsActive): Attribute[IsActive] = + Attribute(name, isActive, AS_IS) + } + } + + implicit class RouterDOMVirtualDOMAttributes(attributes: VirtualDOMAttributes) { + + import RouterDOMVirtualDOMAttributes._ + + lazy val activeClassName = SpaceSeparatedStringAttributeSpec("activeClassName") + lazy val activeStyle = CssAttributeSpec("activeStyle") + lazy val basename = StringAttributeSpec("basename") + lazy val forceRefresh = BooleanAttributeSpec("forceRefresh") + lazy val getUserConfirmation = GetUserConfirmationAttributeSpec("getUserConfirmation") + lazy val hashType = HashTypeAttributeSpec("hashType") + lazy val isActive = IsActiveAttributeSpec("isActive") + lazy val keyLength = IntegerAttributeSpec("keyLength") + lazy val replace = BooleanAttributeSpec("replace") + } +} + +object RouterDOM extends RouterDOM diff --git a/router/README.md b/router/README.md index 76bb230..31aa9fe 100644 --- a/router/README.md +++ b/router/README.md @@ -1,12 +1,3 @@ # scalajs-reactjs-router -Add routing to your [scalajs-reactjs]() applications. It is a facade for [react-router](https://github.com/ReactTraining/react-router). - -## Examples - -[Click here for an example](/example/routing/src/main/scala/io/github/shogowada/scalajs/reactjs/example/routing/Main.scala). - -## API References - -- [`Router`](./src/main/scala/io/github/shogowada/scalajs/reactjs/router/Router.scala) -- [`RoutedReactClassSpec`](./src/main/scala/io/github/shogowada/scalajs/reactjs/router/RoutedReactClassSpec.scala) +A facade for [react-router](https://github.com/ReactTraining/react-router) diff --git a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/History.scala b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/History.scala index 3e97bb4..6f97dcf 100644 --- a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/History.scala +++ b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/History.scala @@ -12,11 +12,3 @@ trait History extends js.Object { def goBack(): Unit = js.native def goForward(): Unit = js.native } - -@js.native -@JSImport("react-router", "browserHistory") -object BrowserHistory extends History - -@js.native -@JSImport("react-router", "hashHistory") -object HashHistory extends History diff --git a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Location.scala b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Location.scala new file mode 100644 index 0000000..c7936fa --- /dev/null +++ b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Location.scala @@ -0,0 +1,13 @@ +package io.github.shogowada.scalajs.reactjs.router + +import scala.scalajs.js + +@js.native +trait Location extends js.Object { + val action: String = js.native + val key: js.UndefOr[String] = js.native + val hash: String = js.native + val pathname: String = js.native + val search: String = js.native + val state: js.UndefOr[js.Dictionary[js.Any]] = js.native +} diff --git a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Match.scala b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Match.scala new file mode 100644 index 0000000..5cbb2af --- /dev/null +++ b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Match.scala @@ -0,0 +1,11 @@ +package io.github.shogowada.scalajs.reactjs.router + +import scala.scalajs.js + +@js.native +trait Match extends js.Object { + val isExact: Boolean = js.native + val params: js.Dictionary[String] = js.native + val path: String = js.native + val url: String = js.native +} diff --git a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RoutedReactClassSpec.scala b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RoutedReactClassSpec.scala deleted file mode 100644 index ef596ee..0000000 --- a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RoutedReactClassSpec.scala +++ /dev/null @@ -1,40 +0,0 @@ -package io.github.shogowada.scalajs.reactjs.router - -import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec -import io.github.shogowada.scalajs.reactjs.elements.ReactElement - -import scala.scalajs.js - -object RoutedReactClassSpec { - def nativeProps[Params <: js.Object](classSpec: RoutedReactClassSpec[Params]): js.Dynamic = - classSpec.asInstanceOf[ReactClassSpec[_, _]].nativeProps -} - -import io.github.shogowada.scalajs.reactjs.router.RoutedReactClassSpec._ - -/** [[ReactClassSpec]] extension for routed components */ -trait RoutedReactClassSpec[Params <: js.Object] { - - def router: Router = new Router(nativeProps(this).router.asInstanceOf[NativeRouter]) - def location: Location = nativeProps(this).location.asInstanceOf[Location] - def route: ReactElement = nativeProps(this).route.asInstanceOf[ReactElement] - def routes: js.Array[ReactElement] = nativeProps(this).routes.asInstanceOf[js.Array[ReactElement]] - - /** props.params equivalent of native React - * - * {{{ - * object Foo { - * @js.native - * trait Params extends js.Object { - * val foo: String = js.native - * } - * } - * - * class Foo extends StaticReactClassSpec - * with RoutedReactClassSpec[Foo.Params] { - * override def render(): ReactElement = <.div()(params.foo) - * } - * }}} - * */ - def params: Params = nativeProps(this).params.asInstanceOf[Params] -} diff --git a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Router.scala b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Router.scala index 5227832..9e45511 100644 --- a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Router.scala +++ b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/Router.scala @@ -1,110 +1,77 @@ package io.github.shogowada.scalajs.reactjs.router -import io.github.shogowada.scalajs.reactjs.VirtualDOM import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMAttributes.Type.AS_IS +import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMAttributes.{ReactClassAttributeSpec, RenderAttributeSpec} import io.github.shogowada.scalajs.reactjs.VirtualDOM.VirtualDOMElements.ReactClassElementSpec import io.github.shogowada.scalajs.reactjs.VirtualDOM.{VirtualDOMAttributes, VirtualDOMElements} import io.github.shogowada.scalajs.reactjs.classes.ReactClass -import io.github.shogowada.scalajs.reactjs.classes.specs.ReactClassSpec -import io.github.shogowada.scalajs.reactjs.elements.ReactElement -import io.github.shogowada.statictags.{Attribute, AttributeSpec, StringAttributeSpec} +import io.github.shogowada.statictags.{Attribute, AttributeSpec, BooleanAttributeSpec, StringAttributeSpec} import scala.scalajs.js import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js.| @js.native -@JSImport("react-router", "Router") -object NativeRouter extends ReactClass +@JSImport("react-router", "Prompt") +object NativePrompt extends ReactClass + +@js.native +@JSImport("react-router", "Redirect") +object NativeRedirect extends ReactClass @js.native @JSImport("react-router", "Route") object NativeRoute extends ReactClass @js.native -@JSImport("react-router", "IndexRoute") -object NativeIndexRoute extends ReactClass +@JSImport("react-router", "Router") +object NativeRouter extends ReactClass @js.native -@JSImport("react-router", "Link") -object NativeLink extends ReactClass +@JSImport("react-router", "Switch") +object NativeSwitch extends ReactClass -/** Facade for react-router */ -object Router { - /** [[VirtualDOM]] extension for router components - * - * {{{ - * import io.github.shogowada.scalajs.reactjs.VirtualDOM._ - * import io.github.shogowada.scalajs.reactjs.router.Router._ - * - * <.Router(^.history := HashHistory)( - * <.Route(^.path := "/", ^.component := new App())( - * <.Route(^.path := "about", ^.component := new About())(), - * <.Route(^.path := "repos", ^.component := new Repos())( - * <.Route(^.path := ":id", ^.component := new Repo())() - * ) - * ) - * ) - * }}} - * */ - implicit class RichVirtualDOMElements(elements: VirtualDOMElements) { - lazy val Router = ReactClassElementSpec(NativeRouter) +trait Router { + implicit class RouterVirtualDOMElements(elements: VirtualDOMElements) { + lazy val Prompt = ReactClassElementSpec(NativePrompt) + lazy val Redirect = ReactClassElementSpec(NativeRedirect) lazy val Route = ReactClassElementSpec(NativeRoute) - lazy val IndexRoute = ReactClassElementSpec(NativeIndexRoute) - lazy val Link = ReactClassElementSpec(NativeLink) + lazy val Router = ReactClassElementSpec(NativeRouter) + lazy val Switch = ReactClassElementSpec(NativeSwitch) } - implicit class RichVirtualDOMAttributes(attribute: VirtualDOMAttributes) { - case class ReactClassAttributeSpec(name: String) extends AttributeSpec { - def :=[Props, State](value: ReactClassSpec[Props, State]): Attribute[ReactClassSpec[Props, State]] = - Attribute(name, value, AS_IS) - def :=(value: ReactClass): Attribute[ReactClass] = Attribute(name, value, AS_IS) + object RouterVirtualDOMAttributes { + case class MessageAttributeSpec(name: String) extends AttributeSpec { + def :=(message: String) = Attribute[String](name, message) + def :=(message: js.Function0[String]) = Attribute[js.Function0[String]](name, message, AS_IS) } case class HistoryAttributeSpec(name: String) extends AttributeSpec { def :=(value: History): Attribute[History] = Attribute(name, value, AS_IS) } - lazy val component = ReactClassAttributeSpec("component") - lazy val history = HistoryAttributeSpec("history") - lazy val path = StringAttributeSpec("path") - lazy val to = StringAttributeSpec("to") + case class LocationAttributeSpec(name: String) extends AttributeSpec { + def :=(path: String) = Attribute[String](name, path) + def :=(location: Location) = Attribute[Location](name, location, AS_IS) + } } -} -@js.native -trait Location extends js.Object { - val pathname: String = js.native - val search: String = js.native - val state: js.UndefOr[js.Object] = js.native - val action: String = js.native - val key: js.UndefOr[String] = js.native -} - -@js.native -trait NativeRouter extends js.Object { - def push(path: String): Unit = js.native - def replace(path: String): Unit = js.native - def go(delta: Int): Unit = js.native - def goBack(): Unit = js.native - def goForward(): Unit = js.native - - def setRouteLeaveHook(route: ReactElement, hook: js.Function1[Location, Boolean | String]): js.Function0[js.Any] = js.native - - def isActive(path: String): Boolean = js.native -} + implicit class RouterVirtualDOMAttributes(attribute: VirtualDOMAttributes) { -class Router(native: NativeRouter) { - def push(path: String): Unit = native.push(path) - def replace(path: String): Unit = native.replace(path) - def go(delta: Int): Unit = native.go(delta) - def goBack(): Unit = native.goBack() - def goForward(): Unit = native.goForward() + import RouterVirtualDOMAttributes._ - def setRouteLeaveHook(route: ReactElement, hook: Location => Boolean | String): () => Unit = { - val unset = native.setRouteLeaveHook(route, hook) - () => unset() + lazy val children = RenderAttributeSpec("children") + lazy val component = ReactClassAttributeSpec("component") + lazy val exact = BooleanAttributeSpec("exact") + lazy val from = StringAttributeSpec("from") + lazy val history = HistoryAttributeSpec("history") + lazy val message = MessageAttributeSpec("message") + lazy val path = StringAttributeSpec("path") + lazy val push = BooleanAttributeSpec("push") + lazy val render = RenderAttributeSpec("render") + lazy val strict = BooleanAttributeSpec("strict") + lazy val to = LocationAttributeSpec("to") + lazy val when = BooleanAttributeSpec("when") } - - def isActive(path: String): Boolean = native.isActive(path) } + +object Router extends Router diff --git a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RouterProps.scala b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RouterProps.scala new file mode 100644 index 0000000..50115b4 --- /dev/null +++ b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/RouterProps.scala @@ -0,0 +1,13 @@ +package io.github.shogowada.scalajs.reactjs.router + +import io.github.shogowada.scalajs.reactjs.classes.specs.Props + +trait RouterProps { + implicit class RoutedProps(props: Props[_]) { + def history: History = props.native.history.asInstanceOf[History] + def location: Location = props.native.location.asInstanceOf[Location] + def `match`: Match = props.native.`match`.asInstanceOf[Match] + } +} + +object RouterProps extends RouterProps diff --git a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/WithRouter.scala b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/WithRouter.scala index c2cf298..7a5daa6 100644 --- a/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/WithRouter.scala +++ b/router/src/main/scala/io/github/shogowada/scalajs/reactjs/router/WithRouter.scala @@ -8,11 +8,7 @@ import scala.scalajs.js import scala.scalajs.js.annotation.JSImport object WithRouter { - def apply[Props, State](reactClassSpec: ReactClassSpec[Props, State]): ReactClass = { - val reactClass = React.createClass(reactClassSpec) - NativeWithRouter(reactClass) - } - + def apply[WrappedProps, State](reactClassSpec: ReactClassSpec[WrappedProps, State]): ReactClass = this.apply(React.createClass(reactClassSpec)) def apply(reactClass: ReactClass): ReactClass = NativeWithRouter(reactClass) }