Skip to content

Latest commit

 

History

History
304 lines (236 loc) · 12.9 KB

README.md

File metadata and controls

304 lines (236 loc) · 12.9 KB

scalajs-react

Lifts Facebook's React library into Scala.js and endeavours to make it as type-safe and Scala-friendly as possible.

In addition to wrapping React, this provides extra opt-in functionality to support (separately) easier testing, and pure FP.

Additional features not available in React JS itself, are available in the extra module.

Contents

Docs
Requirements:
  • React 0.12
  • Scala 2.11
  • Scala.JS 0.5.4+

Setup

Firstly, you'll need to add Scala.js to your project.

Next, add scalajs-react to SBT:

// Minimal usage
libraryDependencies += "com.github.japgolly.scalajs-react" %%% "core" % "0.7.1"

// React itself
//   (react-with-addons.js can be react.js, react.min.js, react-with-addons.min.js)
jsDependencies += "org.webjars" % "react" % "0.12.1" / "react-with-addons.js" commonJSName "React"

// Test support including ReactTestUtils
//   (requires react-with-addons.js instead of just react.js)
libraryDependencies += "com.github.japgolly.scalajs-react" %%% "test" % "0.7.1" % "test"

// Scalaz support
libraryDependencies += "com.github.japgolly.scalajs-react" %%% "ext-scalaz71" % "0.7.1" // or
libraryDependencies += "com.github.japgolly.scalajs-react" %%% "ext-scalaz70" % "0.7.1"

// Monocle support
libraryDependencies += "com.github.japgolly.scalajs-react" %%% "ext-monocle" % "0.7.1"

// Extra features
libraryDependencies += "com.github.japgolly.scalajs-react" %%% "extra" % "0.7.1"

Code:

// The basics
import japgolly.scalajs.react._

// Virtual DOM building
// There are two flavours. In both examples we will build:
//   <a class="google" href="https://www.google.com"><span>GOOGLE!</span></a>

// Method 1 (recommended): Using prefixes < for tags, ^ for attributes.
import japgolly.scalajs.react.vdom.prefix_<^._
val vdom = <.a(
             ^.className := "google",
             ^.href      := "https://www.google.com",
             <.span("GOOGLE!"))

// Method 2: Importing everything without prefix into namespace.
import japgolly.scalajs.react.vdom.all._
val vdom = a(
             className := "google",
             href      := "https://www.google.com",
             span("GOOGLE!"))

// Scalaz support
import japgolly.scalajs.react.ScalazReact._

// Monocle support
import japgolly.scalajs.react.MonocleReact._

Examples

A number of examples are demonstrated online here.

You'll find that nearly all of the demos in the React doc are on display beside their Scala counterparts. If you know Scala and React, you should be up and running in no time.

The source code for the above lives here. To build and play around with locally:

  1. Checkout or download this repository.
  2. sbt gh-pages/fastOptJS
  3. Open gh-pages/index.html locally.

Differences from React proper

  • Rather than using JSX or React.DOM.xxx to build a virtual DOM, a specialised copy of Scalatags is used. (See examples.)
  • In addition to props and state, if you look at the React samples you'll see that most components need additional functions and in the case of sample #2, state outside of the designated state object (!). In this Scala version, all of that is heaped into an abstract type called Backend which you can supply or omit as necessary.
  • To keep a collection together when generating the dom, call .toJsArray. The only difference I'm aware of is that if the collection is maintained, React will issue warnings if you haven't supplied key attributes. Example:
    table(tbody(
      tr(th("Name"), th("Description"), th("Etcetera")),
      myListOfItems.sortBy(_.name).map(renderItem).toJsArray
    ))
  • To specify a key when creating a React component, instead of merging it into the props, call .set(key = ...) before providing the props and children.
    val Example = ReactComponentB[String]("Eg").render(i => h1(i)).build
    Example.set(key = "key1")("The Prop")

MOAR FP / Scalaz

Included is a Scalaz module that facilitates a more functional and pure approach to React integration. This is achieved primarily via state and IO monads. Joyously, this approach makes obsolete the need for a "backend".

State modifications and setState callbacks are created via ReactS, which is conceptually WriterT[M, List[Callback], StateT[M, S, A]]. ReactS monads are applied via runState. Vanilla StateT monads (ie. without callbacks) can be lifted into ReactS via .liftR. Callbacks take the form of IO[Unit] and are hooked into HTML via ~~>, e.g. button(onclick ~~> T.runState(blah), "Click Me!").

Also included are runStateF methods which use a ChangeFilter typeclass to compare before and after states at the end of a state monad application, and optionally opt-out of a call to setState on a component.

See ScalazExamples for a taste. Take a look at the ScalazReact module for the source.

Monocle

A module with a extensions for Monocle also exists under ext-monocle.

Testing

React.addons.TestUtils is wrapped in Scala and available as ReactTestUtils in the test module (see Setup). Usage is unchanged from JS.

To make event simulation easier, certain event types have dedicated, strongly-typed case classes to wrap event data. For example, JS like

// JavaScript
React.addons.TestUtils.Simulate.change(t, {target: {value: "Hi"}})

becomes

// Scala
ReactTestUtils.Simulate.change(t, ChangeEventData(value = "Hi"))

// Or shorter
ChangeEventData("Hi") simulate t

Simulations can also be created and composed without a target, using Simulation. Example:

val a = Simulation.focus
val b = Simulation.change(ChangeEventData(value = "hi"))
val c = Simulation.blur
val s = a andThen b andThen c

// Or shorter
val s = Simulation.focus >> ChangeEventData("hi").simulation >> Simulation.blur

// Or even shorter again, using a convenience method
val s = Simulation.focusChangeBlur("hi")

// Then run it later
s run component

DOM lookup is much easier than using ReactTestUtils directly by instead using Sel. Sel allows you to use a jQuery/CSS-like selector to lookup a DOM element or subset. Full examples can be seen here; this is a sample:

val dom = Sel(".inner a.active.new") findIn myComponent

Also included is DebugJs, a dumping ground for functionality useful when testing JS. inspectObject can be tremendously useful.

SBT

In order to test React and use ReactTestUtils you will need to make a few changes to SBT.

  • Add
jsDependencies += "org.webjars" % "react" % "0.12.1" % "test"
                  / "react-with-addons.js" commonJSName "React"

requiresDOM := true

test      in Test := (test      in(Test, fastOptStage)).value

testOnly  in Test := (testOnly  in(Test, fastOptStage)).evaluated

testQuick in Test := (testQuick in(Test, fastOptStage)).evaluated
  • Install PhantomJS.

Extensions

Scalatags

  • Case of attributes and styles matches React. So unlike vanilla-Scalatags' onclick attribute, use onClick.
  • attr ==> (SyntheticEvent[_] => _) - Wires up an event handler.
    def handleSubmit(e: SyntheticEvent[HTMLInputElement]) = ...
    val html = form(onsubmit ==> handleSubmit)(...)
  • attr --> (=> Unit) - Specify a function as an attribute value.
    def reset() = T.setState("")
    val html = div(onclick --> reset())("Click to Reset")
  • boolean ?= (attr := value) - Make a condition optional.
    def hasFocus: Boolean = ...
    val html = div(hasFocus ?= (cls := "focus"))(...)
  • Attributes, styles, and tags can be wrapped in Option or js.UndefOr to make them optional.
    val person: js.UndefOr[Person] = ???
    val name: Option[String] = ???
    val html = div(key := person.map(_.id), value := name)
  • EmptyTag - A virtual DOM building block representing nothing.
  div(if (allowEdit) editButton else EmptyTag)
  • Custom tags, attributes and styles.
  val a = "customAttr" .reactAttr
  val s = "customStyle".reactStyle
  val t = "customTag"  .reactTag

  // <customTag customAttr="hello" style="customStyle:123;">bye</customTag>
  t(a := "hello", s := "123", "bye")

React

  • Where this.setState(State) is applicable, you can also run modState(State => State).
  • SyntheticEvents have aliases that don't require you to provide the dom type. So instead of SyntheticKeyboardEvent[xxx] type alias ReactKeyboardEvent can be used.
  • The component builder has a propsDefault method which takes some default properties and exposes constructor methods that 1) don't require any property specification, and 2) take an Optional[Props].
  • The component builder has a propsAlways method which provides all component instances with given properties, doesn't allow property specification in the constructor.
  • React has a classSet addon for specifying multiple optional class attributes. The same mechanism is applicable with this library is as follows:
    div(classSet(
      "message"           -> true,
      "message-active"    -> true,
      "message-important" -> props.isImportant,
      "message-read"      -> props.isRead
    ))(props.message)

    // Or for convenience, put all constants in the first arg:
    div(classSet1("message message-active"
      ,"message-important" -> props.isImportant
      ,"message-read"      -> props.isRead
    ))(props.message)
  • Sometimes you want to allow a function to both get and affect a portion of a component's state. Anywhere that you can call .setState() you can also call focusState() to return an object that has the same .setState(), .modState() methods but only operates on a subset of the total state.
    def incrementCounter(s: ComponentStateFocus[Int]) = s.modState(_ + 1)

    // Then later in a render() method
    val f = T.focusState(_.counter)((a,b) => a.copy(counter = b))
    button(onclick --> incrementCounter(f))("+")
Refs

Rather than specify references using strings, the Ref object can provide some more safety.

  • Ref(name) will create a reference to both apply to and retrieve a plain DOM node.
  • Ref.to(component, name) will create a reference to a component so that on retrieval its types are preserved.
  • Ref.param(param => name) can be used for references to items in a set, with the key being a data entity's ID.
  • Because refs are not guaranteed to exist, the return type is wrapped in js.UndefOr[_]. A helper method tryFocus() has been added to focus the ref if one is returned.
    val myRef = Ref[HTMLInputElement]("refKey")

    class Backend(T: BackendScope[_, _]) {
      def clearAndFocusInput() = T.setState("", () => myRef(t).tryFocus())
    }

Additional features are available in the extra module.

Gotchas

  • table(tr(...)) will appear to work fine at first then crash later. React needs table(tbody(tr(...))).
  • React doesn't apply invocations of this.setState until the end of render or the current callback. Calling .state after .setState will return the original value, ie. val s1 = x.state; x.setState(s2); x.state == s1 // not s2. If you want to compose state modifications (and you're using Scalaz), take a look at the ScalazReact module, specifically ReactS and runState.

Alternatives

Major differences:

  • Object-oriented approach.
  • Uses XML-literals instead of Scalatags. Resembles JSX very closely.