Scala.js facade for the Preact JavaScript library.
Add the following lines into your build.sbt
file:
libraryDependencies ++= Seq(
"com.github.lmnet" %%% "scala-js-preact-core" % "0.2.1",
"com.github.lmnet" %%% "scala-js-preact-dsl-tags" % "0.2.1"
)
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-M10" cross CrossVersion.full)
scalacOptions += "-P:scalajs:sjsDefinedByDefault"
Now you can create Preact component:
import org.scalajs.dom.Event
import preact.Preact.VNode
import preact.macros.PreactComponent
object SomeComponent {
case class Props(name: String)
}
@PreactComponent[SomeComponent.Props, Unit]
class SomeComponent {
import preact.dsl.tags._
def render(): VNode = {
div(id := "foo",
span(s"Hello, ${props.name}!"),
button(onclick := { _: Event => println("hi!") }, "Click Me")
)
}
}
And render it:
import org.scalajs.dom
import preact.Preact
import scala.scalajs.js.annotation.JSExportTopLevel
object App {
@JSExportTopLevel("App.main")
def main(): Unit = {
val appDiv = dom.document.getElementById("app")
val component = SomeComponent(name = "scala-js-preact user")
Preact.render(component, appDiv)
}
}
You can find more examples in the examples directory.
Why should you care about this "yet another Scala.js facade for React"? The current situation in the Scala.js ecosystem
forced developers to spend quite a lot of time to be able to write frontend applications with React facades.
Also, React is a pretty heavy library and Scala.js runtime also adds its own overhead.
Even after fullOptJS
and with gzip
it's common to have more than 1 MB of JavaScript code in the bundle.
scala-js-preact
is trying to solve this issues:
- Preact instead of React. Preact is a fast 3KB alternative to React, with the same API. It helps to minimize js bundle size.
- Minimalistic and simple.
scala-js-preact
contains a small amount of code and this also helps to prevent your bundle overgrowth. Also, it doesn't contain any complex functional stuff in the API which is good for the newcomers. - Modular architecture. Use only parts you need in your application.
- Familiar API. Scala API should be more or less like JavaScript API.
- DSL agnostic. Scala is the very powerful language in terms of DSL. But this power also lead to a lot of debates.
scala-js-preact
doesn't limit you with the single HTML DSL. It provides a few DSL out of the box, but you can easily create your own DSL, or use third-party DSL.
scala-js-preact-raw
- raw Preact facade. Just some low-level typings. You will need this module only if you want to create your own Preact library.scala-js-preact-core
- core module. Contains APIs for creating class and function components. This is the main module of the library. To use this module you need to add macro-paradise compiler plugin becausecore
uses some macro magic to provide nice API for creating components.scala-js-preact-dsl-symbol
- minimalistic DSL module with dynamically typed tags and attributes. Inspired by levsha.scala-js-preact-dsl-tags
- DSL module with typesafe tags and attributes. Inspired by scalatags.
To use scala-js-preact
you should add this line into your build.sbt file:
scalacOptions += "-P:scalajs:sjsDefinedByDefault"
You can read more about this scalac option here.
This requirement is temporary: after release Scala.js 1.0 this option will be always enabled without explicit configuration.
To create a class component you should annotate your class with @PreactComponent[PropsType, StateType]
annotation.
You should set up Props
and State
types of the component:
case class Props(...)
case class State(...)
@PreactComponent[Props, State]
class SomeComponent {...}
If your component doesn't use Props
or State
you can set any of them as Unit
:
@PreactComponent[Props, Unit]
class StatelessComponent {...}
@PreactComponent[Unit, State]
class PropslessComponent {...}
@PreactComponent[Unit, Unit]
class ConstantComponent {...}
IMPORTANT NOTE! You should always wrap Props
and State
in the class (or case class), even if it has only one parameter. This is necessary because these objects are handled by Preact's javascript code, which could mutate your immutable scala code from js side and cause undefined behavior.
The only one required method in the components is render
:
@PreactComponent[Props, State]
class SomeComponent {
def render(): VNode = ???
}
Here is the list of other Component
methods:
props
- gives access to the read-onlyprops
object.state
- gives access to the read-onlystate
object.setState
- method to define a new state of the component. This is the only way to set up a new state.initialState
- if you want to define the initial state of the component, you should do this with this method. This method could be called only once.children
- gives access to the component's children.key
- gives access to the component's key.base
- gives access to the component's DOM node.componentWillMount
- lifecycle method.componentDidMount
- lifecycle method.componentWillUnmount
- lifecycle method.componentWillReceiveProps
- lifecycle method.shouldComponentUpdate
- lifecycle method.componentWillUpdate
- lifecycle method.componentDidUpdate
- lifecycle method.
If your component use props, creating a component instance usually looks like this:
case class Props(foo: String, bar: Int)
@PreactComponent[Props, Unit]
class SomeComponent {
def render(): VNode = ???
}
val instance = SomeComponent(Props("test", 15))
But, its a good practice to place local Props
and State
case classes inside companion object of the component.
If you do this, @PreactComponent
macro will generate additional apply
method with the same signature as Props
case class has:
object SomeComponent {
case class Props(foo: String, bar: Int)
}
@PreactComponent[SomeComponent.Props, Unit]
class SomeComponent {
def render(): VNode = ???
}
val instance = SomeComponent("test", 15) // don't need to wrap argument in Props(...)
It makes your code a little more readable.
There is some unobvious moment with getting props from the component's constructor. This is the common pattern to set up initial state from the props from the constructor. Let's try to write some code with this pattern:
case class Props(foo: Int)
case class State(bar: String)
@PreactComponent[Props, State]
class SomeComponent(props: Props) {
initialState(State(
bar = props.foo.toString
))
def render(): VNode = {
div(props.foo.toString, state.bar)
}
}
At first glance, everything looks good. But, what about props
method? In the component above we have a naming conflict:
props
object from the constructor and props
method in the same scope. To prevent this issue we should rename
our constructor argument, for example like this:
@PreactComponent[Props, State]
class SomeComponent(initialProps: Props) {
initialState(State(
bar = initialProps.foo.toString
))
def render(): VNode = {
div(props.foo.toString, state.bar)
}
}
Now we don't have naming conflict and can use both initialProps
and props
.
Good thing to know: @PreactComponent
macro checks this and if you got into this problem you will have
nice compile-time error.
scala-js-preact
got two optional DSLs out of the box: symbol DSL and tags DSL. You can use any of them, or any third party DSL,
or even create your own DSL. In the sections below you will be guided how to use default DSLs and create your own.
This DSL is inspired by levsha DSL. Its goal is to be minimalistic and not typesafe in terms of HTML tags or attributes. You can use any tags and attributes you want, like in normal HTML. All under your control and responsibility.
To use this DSL add it to your dependencies:
"com.github.lmnet" %%% "scala-js-preact-dsl-symbol" % "0.2.1"
And import preact.dsl.symbol._
into your source code:
import preact.dsl.symbol._
'section("class" -> "todoapp",
'header("class" -> "header",
'h1("todos")
)
)
This DSL is very simple:
// Any `scala.Symbol` became tag with `apply` method:
'div()
// Tuples inside tags became attributes:
'div(
"class" -> "foo"
)
// Functions works too:
def callback(event: Event): Unit = {
???
}
'div(
"onclick" -> callback _
)
// You can pass any VNode, component or strings inside tags:
'div(someComponentInstance, "some text", 'b("some another tag"))
// Also, there are a few helpers:
'div(
if (something) {
Entry.Children(Seq(???)) // gives possibility to pass `Iterable[Preact.Child]` to any tag
} else {
Entry.EmptyChild // useful for conditional children
},
if (somethingElse) {
"foo" -> "bar"
} else {
Entry.EmptyAttribute // useful for conditional attributes
}
)
This DSL is inspired by scalatags DSL. Its goal is to provide typesafe HTML DSL, and at the same time be minimalistic as possible.
Why not just create scalatags
backend for the scala-js-preact
? Here are a few reasons:
- Annoying
.render
on all fragments. scalatags
is small, but not enough small. It contains some parts that I don't want to have in thescala-js-preact
DSL, like styles.- Because of
scalatags
internals and rendering process, it will add some extra performance overhead.
But if you want you can create scalatags
backend for the scala-js-preact
. Contributions are welcome!
To use this DSL add it to your dependencies:
"com.github.lmnet" %%% "scala-js-preact-dsl-tags" % "0.2.1"
And import preact.dsl.tags._
into your source code:
import preact.dsl.tags._
section(`class` := "todoapp",
header(`class` := "header",
h1("todos")
)
)
The main idea is the same as in the scalatags
. I suggest you to familiarize with its docs at first.
The main difference between scalatags
and scala-js-preact-dsl-tags
:
- You don't need to call
.render
method on your fragments. Tag
'sapply
method returns and receivesPreact.VNode
.- Has
Children
,EmptyAttribute
andEmptyChild
, just like Symbol DSL.
Every Preact DSL is based on the single Preact.raw.h
function under the hood.
This function creates VNode
instances.
It has the following signatures (in the real source code they are looking a little different):
type NodeType = js.Dynamic | // for class components
js.Function0[VNode] | // for function components without arguments
js.Function1[js.Dynamic, VNode] | // for function components with props argument
String // for HTML tags
// for VNode with children
def h(
node: NodeType,
params: js.Dictionary[js.Any], // if params is empty you should pass null
children: Child*
): VNode
// for VNode without children
def h(
node: NodeType,
params: js.Dictionary[js.Any] // if params is empty you should pass null
): VNode
Important note: in the Preact (and React) null
is the correct child node. In your DSL you should correctly preserve
an order of all child nodes, including null
. You can replace null
with something else in your high-level DSL API,
but on the low-level Preact.raw.h
call you should pass null
. Look at the Entry.EmptyChild
in the symbol or tags
DSLs for the example.
This is all you should know about creating DSLs for scala-js-preact
.
You can check code of the built-in DSLs for the real examples.
This project is in the pre-alpha state. Please, don't use it in the production!