This is just a facade for React, so if you are not already familiar with React, I recommend getting familiar with React first.
- How does it replace JSX in Scala?
- How to create React classes?
- What's WrappedProps?
- How about plain JS props?
- How about states?
- Can I see fully working examples?
To create elements in Scala, import VirtualDOM._
. VirtualDOM
is an extended Static Tags.
VirtualDOM
is made of three parts: tag, attributes (a.k.a. props), and children.
For example, this code
<.div(^.id := "hello-world")("Hello, World!")
is equivalent of the following:
<div id="hello-world">Hello, World!</div>
You can use as many as attributes and children you want.
If <
and ^
look weird to you, you can change it, but otherwise think of <
as opening a tag (<
of <div>
) and ^
as attaching an attribute to the tag.
You can also get rid of the prefixes altogether by importing <._
and ^._
, but it is not recommended because many HTML tag and attribute names are concise and tend to conflict with other variable names (e.g. id
attribute, map
tag).
Visit the Static Tags page for more techniques (e.g. using dynamic tags and attributes, flattening tags and attributes).
Use React.createClass[WrappedProps, State]
to create React classes (we will explain about WrappedProps
in next section).
val reactClass: ReactClass = React.createClass[Unit, Unit]( // If you don't have props or state, use Unit.
(self) => <.div()("Hello, World!")
)
In addition to render
function, it also supports all the functions React.Component
supports except defaultProps
.
The first argument of each function must be Self[WrappedProps, State]
, props must have type Props[WrappedProps]
, and states must have type State
.
For example, shouldComponentUpdate(nextProps, nextState)
function will be shouldComponentUpdate(self: Self[WrappedProps, State], nextProps: Props[WrappedProps], nextState: State): Boolean
:
case class WrappedProps(/* ... */)
case class State(/* ... */)
type Self = React.Self[WrappedProps, State]
type Props = React.Props[WrappedProps]
val reactClass: ReactClass = React.createClass[WrappedProps, State](
shouldComponentUpdate = (self: Self, nextProps: Props, nextState: State) => {/* do something */},
render = (self: Self) => <.div()("Hello, World!")
)
Self[WrappedProps, State]
is equivalent of this
in React, so you can do things like self.props.children
or self.setState(State(foo = "bar"))
. Unlike React, we are just giving it as input of the functions rather than class members.
To render React classes, use <(/* ReactClass */)
to make it a virtual DOM. You can pass attributes and children like regular elements.
import io.github.shogowada.scalajs.reactjs.VirtualDOM._
val reactClass = React.createClass(/* ... */)
ReactDOM.render(
<(reactClass)(
// attributes (a.k.a. props)
^.id := "my-component"
)(
// children
<.div()("I'm your component's child!")
),
mountNode
)
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 them in another property. In this facade, we wrap them in "wrapped" property.
import io.github.shogowada.scalajs.reactjs.ReactDom
import io.github.shogowada.scalajs.reactjs.VirtualDOM._
case class WrappedProps(foo: String, bar: Int)
val reactClass = React.createClass[WrappedProps, Unit](
(self) =>
<.div()(
s"foo: ${self.props.wrapped.foo}",
<.br.empty,
s"bar: ${self.props.wrapped.bar}"
)
)
ReactDOM.render(
<(reactClass)(^.wrapped := WrappedProps("foo", 123))(),
mountNode
)
Alternatively to case classes and Wrapped
props you
can also use plain JavaScript object as props directly.
However, you have to define it as non-native JS trait:
import io.github.shogowada.scalajs.reactjs.ReactDom
import io.github.shogowada.scalajs.reactjs.VirtualDOM._
import scala.scalajs.js
trait PlainJSProps extends js.Object {
val foo: String
val bar: Int
}
object PlainJSProps {
def apply(foo: String, bar: Int): PlainJSProps = {
js.Dynamic.literal(
foo = foo,
bar = bar
).asInstanceOf[PlainJSProps]
}
}
val reactClass = React.createClass[PlainJSProps, Unit](
(self) =>
<.div()(
s"foo: ${self.props.plain.foo}",
<.br.empty,
s"bar: ${self.props.plain.bar}"
)
)
ReactDOM.render(
<(reactClass)(^.plain := PlainJSProps("foo", 123))(),
mountNode
)
Props looks like the following:
case class Props[T](native: js.Dynamic) {
def plain: T = native.asInstanceOf[T]
def wrapped: T = native.wrapped.asInstanceOf[T]
def children: ReactElement = native.children.asInstanceOf[ReactElement]
}
You can extend it as you see needs. See RouterProps
for an example.
States are wrapped and unwrapped automatically, so you don't need to do state.wrapped
. We can wrap and unwrap states automatically because nothing extends states; it is supposed to be local to each component.
import io.github.shogowada.scalajs.reactjs.ReactDomVirtualDOM._
import io.github.shogowada.scalajs.reactjs.VirtualDOM._
case class State(text: String)
val reactClass = React.createClass[Unit, State](
getInitialState = (self) => State(text = ""),
render = (self) =>
<.div()(
<.input(
^.placeholder := "Type something here",
^.value := self.state.text,
^.onChange := onChange(self)
)()
)
)
def onChange(self: Self[Unit, State]) = // Use a higher-order function (a function returning a function)
(event: FormSyntheticEvent[HTMLInputElement]) => {
val newText = event.target.value // Cache the value because React reuses events
self.setState(_.copy(text = newText)) // Shortcut for self.setState((prevState: State) => prevState.copy(text = newText))
}
Sure, you can! All the projects in this folder are fully working examples, but here are some top picks: