Scala.js Cookbook
The official Scala.js docs are actually quite good. But the following cookbook contains specific use cases that you may come across quite often (that may be tangential to Scala.js itself and for which there may be little documentation) and some of the caveats you’ll find when declaring Scala.js types, that you’d otherwise find out about when the compiler yells at you
Defining JS types in Scala
docs.
You must have scalacOptions += "-P:scalajs:sjsDefinedByDefault"
in your client project declaration in
build.sbt
(as @ScalaJSDefined
annotations are now deprecated)
trait Foo extends js.Object {
val bar: String
val baz: Int
}
You can now cast js.Object
s to your Scala.js type via jsObject.asInstanceOf[Foo]
. Note unlike what you’re likely used to
that happens when you call .asInstanceOf[Foo]
i.e. an exception being throw immediately, here only when you reference a member
i.e foo.bar
or foo.baz
will an exception be thrown
NOTE: Foo
here isn’t a “facade”, but rather a “Scala.js-defined JS type” (as both are referred to) i.e. a declaration that will be compiled to a plain JS object. These are effectively JS types that whereby their members are visible from JS code, a constructor for them can be exported, and if this is done, JS classes can extend them.
To define a “facade”, that is to say an interface to some JS API. You must do:
@js.native
trait FooAPI extends js.Object {
def bar(x: String) = js.native
}
Find more info about defining facades here
It’s also worth looking at how how Scala types map to Scala.js types: https://www.scala-js.org/doc/interoperabi
lity/types.html , This is particularly important when defining facades. For example if you have an instance of a facade
Bar
with single member val foo: js.Function1[js.Function1[String, String], Int]
as
js.Dynamic.literal(foo = (f: Function1[String, String]) => 3).asInstanceOf[Bar]
you’ll get a cryptic cast exception at runtime d
ue to the fact that a scala Function1
is not a js.Function1
ADTs (Algebraic Data Types)
If you’d like Foo
from above to be a member of some ADT, lets say Quux
, the only way I could conceive of doing this, that most closely mirrors how you declare ADTs in Scala, is:
sealed trait Quux extends js.Any
// as above
trait Foo extends js.Object with Quux { ... }
However this likely will not be very useful to you. You can’t pattern match on a Foo
for example as the compiler complains that it is a raw JS trait. Also case modifiers are not allowed on classes that extend js.Object
. So your best bet oftentimes is to cast to js.Dynamic
and access the fields you need to construct your ADT members precariously:
// Regular Scala ADT
sealed trait Quux
case class Foo(x: String, y: Int) extends Quux
case class Bar(z: String) extends Quux
object Quux {
def fromJSObject(obj: js.Object): Option[Quux] = {
val dyn = obj.asInstanceOf[js.Dynamic]
dyn.blah.asInstanceOf[String] match {
case "identifiesAFoo" =>
Some(Foo(dyn.x.asInstanceOf[String], dyn.y.asInstanceOf[Int]))
case "identifiesABar" =>
Some(Bar(dyn.z.asInstanceOf[String]))
case _ => None
}
}
}
Obviously this will only work if your ADT is shallow. Otherwise you’re going to just have to bite the bullet and declare your
ADT as a scaljs defined type, rather than vanilla Scala. The reason the runtime representation of case classes isn’t, in the case of Foo
above for example, var foo = {x: "hello", y: 5}
taking a random instance but rather: `var foo = {x$1: “hello”, y$2: 5}. This is due to cross compilation consistency with the JVM.
Miscellaneous
Adding JS, CSS, IMG, etc static assets
In build.sbt
:
lazy val client
...
npmAssets ++= NpmAssets
.ofProject(client) { nodeModules: sbt.File =>
(nodeModules / "bootstrap" / "dist" / "css").allPaths +++
(nodeModules / "react-select" / "dist").allPaths +++
(nodeModules / "react-octicons-svg" / "dist").allPaths
}
.value,
...
Lets say you wanted to add bootstrap.js
which lives under
node_modules/bootstrap/dist/js/bootstrap.js
You would add
the following line: (nodeModules / "bootstrap").allPaths
in place of (nodeModules / "bootstrap" / "dist" / "css").allPaths
And then dont forget to add::
<script type="text/javascript" src="/public/bootstrap/dist/js/bootstrap.js"></script>
to
src/main/public/index.html`
Otherwise the asset will not be returned when you do your initial GET on
<host>:<port>/
after starting the server
Comments