This is an experimental library that provides facades for Mithril.
At the moment, scalajs-mithril is being rewritten to support mithril 1.1.1
.
The 0.1.0
version of scalajs-mithril for mithril 0.2.5
can be
found here.
Mithril 1.x.y is significantly different from 0.2.0, which is why this rewrite is required.
Add scalajs-bundler
to project/plugins.sbt
:
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.7.0")
Then, add the following lines to build.sbt
:
resolvers += Resolver.sonatypeRepo("snapshots")
libraryDependencies += "co.technius" %%% "scalajs-mithril" % "0.2.0-SNAPSHOT"
enablePlugins(ScalaJSBundlerPlugin)
// Change mithril version to any version supported by this library
npmDependencies in Compile += "mithril" -> "1.1.1"
Build your project with fastOptJS::webpack
.
import co.technius.scalajs.mithril._
import scala.scalajs.js
import org.scalajs.dom
object MyModule {
val component = Component.stateful[State, js.Object](_ => new State) { vnode =>
import vnode.state
m("div", js.Array[VNode](
m("span", s"Hi, ${state.name()}!"),
m("input[type=text]", js.Dynamic.literal(
oninput = m.withAttr("value", state.name),
value = state.name()
))
))
}
class State {
val name = MithrilStream("Name")
}
}
object MyApp extends js.JSApp {
def main(): Unit = {
m.mount(dom.document.getElementById("app"), MyModule.component)
}
}
<!DOCTYPE HTML>
<html>
<head>
<title>scalajs-mithril Example</title>
</head>
<body>
<div id="app"></div>
<script src="example-fastopt-bundle.js"></script>
</body>
</html>
See the examples folder for complete examples.
This section assumes you are familiar with mithril. If you aren't, don't worry; mithril can be picked up very quickly.
A component is parametized on State
(for vnode.state
) and Attrs
(for
vnode.attrs
). If State
and Attrs
are not neccessary for the component, use
js.Object
and js.Dictionary[js.Any]
should be used instead, respectively.
There are two ways to define a component using this library:
- Use one of the component helpers, such as
Component.stateful
orComponent.viewOnly
. - Subclass
Component
.
While the helpers provide limited control over components, they are sufficiently
powerful for most situations. If more control over a component is desired (e.g.
overriding lifecycle methods), then subclass Component
instead.
Virtual DOM nodes (vnodes) are defined as GenericVNode[State, Attr]
. For
convenience, a type alias VNode
is defined as GenericVNode[js.Object, js.Dictionary[js.Any]]
to reduce the hassle of adding type signatures for
vnodes.
Defining a stateless component is very simple using the Component.viewOnly
function:
import co.technius.scalajs.mithril._
import scala.scalajs.js
object HelloApp extends js.JSApp {
// viewOnly has the view function as an argument
val component = Component.viewOnly[js.Object] { vnode =>
m("div", "Hello world!")
}
def main(): Unit = {
m.mount(dom.document.getElementById("app"), component)
}
}
viewOnly
is parameterized on Attr
, so it's possible to handle arguments:
// First, create a class to represent the attriute object
case class Attr(name: String)
// Then, supply it as a type parameter to viewOnly
val component = Component.viewOnly[Attr] { vnode =>
m("div", "Hello " + vnode.attr.name)
}
It's more common to see stateful components, though. They can be defined using
Component.stateful
.
import co.technius.scalajs.mithril._
import scala.scalajs.js
object NameApp extends js.JSApp {
// Just like for attributes, define a state class to hold the state
protected class State {
var name = "Name"
}
// stateful has two arguments:
// - A function to create the state from a vnode
// - The view function
// It's also parameterized on state and attributes
val component = Component.stateful[State, js.Object](_ => new State) { vnode =>
import vnode.state
m("div", js.Array[VNode](
m("span", s"Hi, ${state.name}!"),
m("input[type=text]", js.Dynamic.literal(
oninput = m.withAttr("value", newName => state.name = newName),
value = state.name
))
))
}
def main(): Unit = {
m.mount(dom.document.getElementById("app"), component)
}
}
Subclassing component requires more boilerplate, but it gives more fine-grained control over the component's lifecycle.
First, you'll need to define your component, which is parametized on State
(for vnode.state
) and Attrs
(for vnode.attrs
). If State
and Attrs
are
not neccessary for the component, use js.Object
.
object MyComponent extends Component[js.Object, js.Object] {
// RootNode is defined as an alias
override val view = (vnode: RootNode) => {
m("div", js.Array(
m("p", "Hello world!")
m("p", "How fantastic!")
))
}
}
Component
is a subtrait of Lifecycle
, which defines the lifecycle methods.
Thus, it's possible to override the lifecycle methods in a Component
. Here's
an example of a stateful component that overrides oninit
to set the state:
object MyComponent extends Component[MyComponentState, js.Object] {
override val oninit = js.defined { (vnode: RootNode) =>
vnode.state = new MyComponentState
}
override val view = { vnode: RootNode =>
import vnode.state
m("div", js.Array(
m("span", s"Hey there, ${state.name()}!"),
m("input[type=text]", js.Dynamic.literal(
oninput = m.withAttr("value", state.name),
value = ctrl.name()
))
))
}
}
class MyComponentState {
val name = MithrilStream("Name")
}
Due to the way mithril handles the fields in the component, runtime errors occur if methods or functions are defined directly in the component from Scala.js. One possible workaround is to define the functions in an inner object:
object MyComponent extends Component[MyComponentState, js.Object] {
override val oninit = js.defined { (vnode: RootNode) =>
vnode.state = new MyComponentState
}
override val view = { (vnode: RootNode) =>
import helpers._
myFunction(vnode.state)
/* other code omitted */
}
object helpers {
def myFunction(state: MyComponentState): Unit = {
// do stuff
}
}
}
class MyComponentState { /* contents omitted */ }
Lastly, call m.mount
with your controller:
import co.technius.scalajs.mithril._
import org.scalajs.dom
import scala.scalajs.js
object MyApp extends js.JSApp {
def main(): Unit = {
m.mount(dom.document.getElementById("app"), MyComponent)
}
}
To use Attrs
in a component, define a class for Attrs
and change the
parameter on Component
. The component should then be created by calling
m(component, attrs)
(see the TreeComponent example).
import co.technius.scalajs.mithril._
case class MyAttrs(name: String)
object MyComponent extends Component[js.Object, MyAttrs] {
override val view = { (vnode: RootNode) =>
m("span", vnode.attrs.name)
}
}
To use Mithril's routing functionality, use m.route
as it is defined in mithril:
import co.technius.scalajs.mithril._
import org.scalajs.dom
import scala.scalajs.js
object MyApp extends js.JSApp {
val homeComponent = Component.viewOnly[js.Object] { vnode =>
m("div", "This is the home page")
}
val pageComponent = Component.viewOnly[js.Object] { vnode =>
m("div", "This is another page")
}
def main(): Unit = {
val routes = js.Dictionary[MithrilRoute.Route](
"/" -> homeComponent,
"/page" -> pageComponent
)
m.route(dom.document.getElementById("app"), "/", routes)
}
}
For convenience, there is an alias for m.route
that accepts a vararg list of
routes instead of a js.Dictionary
:
m.route(dom.document.getElementById("app"), "/", routes)(
"/" -> homeComponent,
"/page" -> pageComponent
)
A RouteResolver
may be used
for more complicated routing situations. There are two ways to construct a
RouteResolver
: using a helper method or subclassing RouteResolver
.
RouteResolver.render
creates a RouteResolver
with the given render
function.
m.route(dom.document.getElementById("app"), "/", routes)(
"/" -> RouteResolver.render { vnode =>
m("div", js.Array[VNode](
m("h1", "Home component"),
homeComponent
))
},
"/page" -> pageComponent
)
Similarly, RouteResolver.onmatch
creates a RouteResolver
with the given
onmatch
function.
val accessDeniedComponent = Component.viewOnly[js.Object] { vnode =>
m("div", "Incorrect or missing password!")
}
val secretComponent = Component.viewOnly[js.Object] { vnode =>
m("div", "Welcome to the secret page!")
}
m.route(dom.document.getElementById("app"), "/", routes)(
"/secret" -> RouteResolver.onmatch { (params, requestedPath) =>
// check if password is correct
if (params.get("password").fold(false)(_ == "12345")) {
secretComponent
} else {
accessDeniedComponent
}
}
)
If it is required to define both render
and onmatch
, subclass
RouteResolver
. Note that this library always ensures that render
is defined,
but allows onmatch
to be undefined.
val helloComponent = Component.viewOnly[js.Object](_ => m("div", "Hello world!"))
val myRouteResolver = new RouteResolver {
override def onmatch = js.defined { (params, requestedPath) =>
helloComponent
}
override def render = { vnode =>
vnode
}
}
First, create an XHROptions[T]
, where T
is the data to be returned:
val opts = new XHROptions[js.Object](method = "GET", url = "/path/to/request")
It's possible to use most of the optional arguments:
val opts =
new XHROptions[js.Object](
method = "POST",
url = "/path/to/request",
data = js.Dynamic.literal("foo" -> 1, "bar" -> 2),
background = true)
Then, pass the options to m.request
, which will return a js.Promise[T]
:
val reqPromise = m.request(opts)
// convert Promise[T] to Future[T]
// use of Future requires implicit ExecutionContext
import scala.concurrent.ExecutionContext.Implicits.global
reqPromise.toFuture.foreach { data =>
println(data)
}
By default, the response data will be returned as a js.Object
. It may be
convenient to define a facade to hold the response data:
// Based on examples/src/main/resources/sample-data.json
import scala.concurrent.ExecutionContext.Implicits.global
@js.native
trait MyData extends js.Object {
val key: String
val some_number: Int
}
val opts = new XHROptions[MyData](method = "GET", url = "/path/to/request")
m.request(opts).toFuture foreach { data =>
println(data.key)
println(data.some_number)
}
There is also support for Scalatags, which can
make it easier to create views. Add the following line to build.sbt
:
libraryDependencies += "co.technius" %%% "scalajs-mithril-scalatags" % "0.2.0-SNAPSHOT"
Then, import co.technius.scalajs.mithril.VNodeScalatags.all._
. If you already
imported the mithril
package as a wildcard, simply import
VNodeScalatags.all._
. You can then use tags in your components:
val component = Component.viewOnly[js.Object] { vnode =>
div(id := "my-div")(
p("Hello world!")
).render
}
It's also possible to use VNode
s with scalatags:
// Components are also VNodes
val embeddedComponent = Component.viewOnly[js.Object] { vnode =>
p("My embedded component").render
}
val component = Component.viewOnly[js.Object] { vnode =>
div(
m("p", "My root component"),
embeddedComponent
).render
}
The lifecycle methods, as well as key
, are usable as attributes in scalatags:
class Attrs(items: scala.collection.mutable.Map[String, String])
val component = Component.viewOnly[Attrs] { vnode =>
ul(oninit := { () => println("Initialized!") })(
vnode.attrs.items.map {
case (id, name) => li(key := id, name)
}
).render
}
See the scalatags demo for an example.
- Compile the core project with
core/compile
. - Examples can be built locally by running
examples/fastOptJS::webpack
and then navigating toexamples/src/main/resources/index.html
. - The benchmarks are built in the same manner as the examples.
benchmarks/fastOptJS::webpack
- To run tests, use
tests/test
.
This library is licensed under the MIT License. See LICENSE for more details.