Last updated on 22.11.2020
UPDATE: Article was updated to Scala.js version 0.6.7, which vastly simplifies Promises related section.
Scala.js opens a big world of frontend development to Scala fans. Most of the time Scala.js project ends up being an independent browser or Node.js application. But there are cases, where you would want to make a library for general frontend developers.
There’re some interesting gotchas in writing Scala.js library such way, that it will be natural to use for an average JS developer. In this article we will develop a simple Scala.js library (code) to work with Github API and will focus on the idiomaticity of it’s JS API.
But first, I’m sure you want to ask
Why would I do that?
Reasonable one.
You should consider developing such a library if:
-
A client application for your Scala API backend already exists, and it’s native Javascript.
Sad, you will hardly have a chance to write it from scratch with Scala.js, but at least it makes sense to write a communication / interpretation library for those guys.
It will simplify interaction between you and frontenders in two ways:- You can hide some tricky client-side logic there, and expose much simpler API.
- Your library can work on model classes, defined in backend project (see Cross-Building). You get typesafe isomorphic code almost for free and can forget about client-server protocol synchronization problems.
-
You develop a public API for developers, like Facebook’s Parse.
A perfect solution for a Javascript API SDK. See all the advantages of the previous case.
Recently, I’ve faced the first case. Moreover, our REST-like JSON API has two different browser based clients. So developing an isomorphic library was a logical choice.
Let’s start with our library.
Requirements
- As Scala developers we want to write all business logic in familiar functional style, being able to use all the handy Scala features.
- Library API must be natural for JS developers.
Setting up project
Such a project doesn’t differ from a regular Scala.js app. If you are new to Scala.js, you can read this tutorial first.
Folder structure:
1 2 3 4 5 6 7 8 9 10 11 12 |
. ├── build.sbt ├── project │ ├── build.properties │ └── plugins.sbt ├── src │ └── main │ ├── resources │ │ ├── demo.js │ │ └── index-fastopt.html │ └── scala └── version.sbt |
resources/index-fastopt.html — a page that will just load our library and resources/demo.js file, that will test the API.
API
The purpose of the library is to simplify Github API interaction. For simplicity, we’ll implement only one feature – loading users and their repos by login.
So it’s, basically, a public method and a pair of model classes, that store results (value objects). Model is the place we’ll start writing code.
Model
Let’s define model classes like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
case class User(name: String, avatarUrl: String, repos: List[Repo]) sealed trait Repo { def name: String def description: String def stargazersCount: Int def homepage: Option[String] } case class Fork(name: String, description: String, stargazersCount: Int, homepage: Option[String]) extends Repo case class Origin(name: String, description: String, stargazersCount: Int, homepage: Option[String], forksCount: Int) extends Repo |
Everything is easy: User has some repos, a repo is either an origin or a fork. Good old Scala model. How do we export that to JS developers?
For a full reference of exporting features see Export Scala.js APIs to Javascript
Object creation API
Let’s look at, how we should expose such API. It seems an easy solution to expose the constructor:
1 2 |
@JSExport case class Fork(name: String, /*...*/) |
But this won’t work. You don’t have Option constructor exported, so there’s no way to create homepage parameter.
Moreover, there are additional limitation for case classes: You can’t export two case constructors that are under inheritance relationship. This code won’t even compile:
1 2 3 4 |
@JSExport case class A(a: Int) @JSExport case class B(b: Int) extends A(12) |
So what is the best choice? I found that it’s best to leave constructors alone and just expose JS-friendly factory methods, like this:
1 2 3 4 5 6 7 8 9 |
@JSExport object Github { @JSExport def createFork(name: String, description: String, stargazersCount: Int, homepage: UndefOr[String] = js.undefined): Fork = Fork(name, description, stargazersCount, homepage.toOption) } |
Here with the help of js.UndefOr we handle optional parameter JS way: you can pass a String , or don’t pass anything:
1 2 3 |
// JS var homelessFork = Github().createFork("bar-fork", "Bar", 1); var fork = Github().createFork("bar-fork", "Bar", 1, "http://foo.bar"); |
Note on caching Scala objects
Making client call Github() every time is not the best API option. If you don’t need laziness, you can cache it upon startup:
1 2 3 |
<!--index-fastopt.html--> <script> var Github = Github() |
Reading model properties
Seamless types
If we now try to read fork’s name, we’ll get undefined . Fair enough, it’s not exported. Let’s export model properties.
There’re no problem with native types like String , Boolean and Int . They can be exported as is:
1 2 3 4 5 |
sealed trait Repo { @JSExport def name: String // ... } |
A case class field can be exported with @(JSExport@field) annotation. An example for forks property, that’s not a member of Repo trait:
1 2 3 4 5 |
case class Origin(name: String, description: String, stargazersCount: Int, homepage: Option[String], @(JSExport@field) forks: Int) extends Repo |
Option
But as you already can expect, there’s a problem with
homepage: Option[String] . Well, we can export it, but this would be useless – to get the actual string value JS developer would have to call something on an option, and nothing is exported.
On the other side, we’d like to keep Option in place, so that our Scala code, that manipulates value classes, remains powerful and simple. js.UndefOr[T] API is way less expressive.
A solution here is to export a special JS-friendly getter method:
1 2 3 4 5 6 7 8 9 10 11 |
import scala.scalajs.js.JSConverters._ sealed trait Repo { //... //leaving unfriendly value hidden def homepage: Option[String] @JSExport("homepage") def homepageJS: js.UndefOr[String] = homepage.orUndefined } |
Let’s try it out, it works:
1 2 |
console.log("fork.name: " + fork.name); console.log("fork.homepage: " + fork.homepage); |
We retained our beloved Option monad, and exported nice and clean JS API. Great!
List
User.repos is a List , and has the same problems with being exported. Solution here is the same too: we’ll just export it as a plain JS Array :
1 2 |
@JSExport("repos") def reposJS: js.Array[Repo] = repos.toJSArray |
Now we can even map them 🙂 :
1 2 3 4 |
// JS user.repos.map(function (repo) { return repo.name; }); |
Sum types
There’s still one problem with Repo trait. As we’re not exporting constructors, given a Repo instance, JS developer can’t figure out, what kind of Repo it is.
In Javascript there’s no pattern matching and using inheritance is not so popular, sometimes even questionable. So we have several options here.
- Depending on the context, provide methods like isFork: Boolean or hasForks: Boolean at the base level. This is perfectly fine, but not general enough.
- Add type: String (or whatever name feels suitable to you) property to all sum types.
I choose the second one, because it can be abstracted and used throughout the whole codebase. Here’s how it can be done. Let’s declare a mixin that exports a type property:
1 2 3 4 |
trait Typed { self => @JSExport("type") def <code>type</code>: String = self.getClass.getSimpleName } |
We have to use a different name for scala definition, because it’s a reserved word.
That’s it! We can now mix it in:
1 2 3 |
sealed trait Repo extends Typed { // ... } |
… and use it:
1 2 |
// JS fork.type // "Fork" |
To make this a little safer, we can store type names constants, that can be compared with instance type property. This can be done typesafe:
1 2 3 4 |
class TypeNameConstant[T: ClassTag] { @JSExport("type") def <code>type</code>: String = classTag[T].runtimeClass.getSimpleName } |
Having this helper class we can define these constants in our Github global for example:
1 2 3 4 5 6 7 |
@JSExportAll object Github { //... val Fork = new TypeNameConstant[model.Fork] val Origin = new TypeNameConstant[model.Origin] } |
Now we can avoid strings in Javascript! An example:
1 2 3 4 |
// JS function isFork(repo) { return repo.type == Github.Fork.type } |
That’s how we dealt with sum types.
What if I can’t change object, that I want to export?
This is a case if you want to (maybe, partially) export your cross-built model classes or other imported library objects. The solution is the same to Option and List with the only difference: you have to implement JS-friendly replacement classes and conversion yourself.
An important rule here is to use JS replacements only for export ( Scala => JS) and instance creation ( JS => Scala ). All business logic must be implemented with pure Scala classes.
Let’s say you have a Commit class, that you can’t change:
1 |
case class Commit(hash: String) |
Here what you can do to export it:
1 2 3 4 5 6 |
object CommitJS { def fromCommit(c: Commit): CommitJS = CommitJS(c.hash) } case class CommitJS(@(JSExport@field) hash: String) { def toCommit: Commit = Commit(hash) } |
Then, for example, a Branch class, that you own, would look like this:
1 2 3 4 |
case class Branch(initial: Commit) { @JSExport("initial") def initialJS: CommitJS = CommitJS.fromCommit(initial) } |
Since in JS environment commits are represented with CommitJS objects, a factory method for Branch would be:
1 2 |
@JSExport def createBranch(initial: CommitJS) = Branch(initial.toCommit) |
Of-course, this workaround is not a beautiful thing, but at least it’s type checked. That’s why I think it’s preferable to view your library not only as a value-classes proxy, but as a facade that hides redundant details and simplifies the API. That way you won’t even need to export the underlying model.
That’s all for exporting model. Let’s move on to the more interesting part – loading the content from Github API.
AJAX
Implementation
For the brevity purposes we will use scalajs-dom Ajax extension as a “network” layer. Let’s for some time forget about how we’re going to export things, let’s just implement the API.
For the simplicity, we’ll put everything AJAX-related into API object. It will have two public methods: for loading user and loading repos.
We will also implement a DTO layer, to decouple API from the model. For type-safe error handling we’ll use Xor type from Cats library. The result type of the method call will be Future[String Xor DTO], where DTO is the type of requested data and String will represent error.
I’ve mentioned everything for this listing to be more understandable, here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
object API { case class UserDTO(name: String, avatar_url: String) case class RepoDTO(name: String, description: String, stargazers_count: Int, homepage: Option[String], forks: Int, fork: Boolean) def user(login: String) (implicit ec: ExecutionContext): Future[String Xor UserDTO] = load(login, s"$BASE_URL/users/$login", jsonToUserDTO) def repos(login: String) (implicit ec: ExecutionContext): Future[String Xor List[RepoDTO]] = load(login, s"$BASE_URL/users/$login/repos", arrayToRepos) private def load[T](login: String, url: String, parser: js.Any => Option[T]) (implicit ec: ExecutionContext): Future[String Xor T] = if (login.isEmpty) Future.successful("Error: login can't be empty".left) else Ajax.get(url).map(xhr => if (xhr.status == 200) { parser(js.JSON.parse(xhr.responseText)) .map(_.right) .getOrElse("Request failed: can't deserialize result".left) } else { s"Request failed with response code ${xhr.status}".left } ) private val BASE_URL: String = "https://api.github.com" private def jsonToUserDTO(json: js.Any): Option[UserDTO] = //... private def arrayToRepos(json: js.Any): Option[List[RepoDTO]] = //... } |
Deserialization code is hidden, it’s not interesting. The load method returns string error, if response code is not 200, otherwise it converts the response data to JSON and then to DTO’s.
Now we can convert our API results into model classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue object Github { // ... def loadUser(login: String): Future[String Xor User] = { for { userDTO <- XorT(API.user(login)) repoDTO <- XorT(API.repos(login)) } yield userFromDTO(userDTO, repoDTO) }.value private def userFromDTO(dto: API.UserDTO, repos: List[API.RepoDTO]): User = //.. } |
Here we use a monad transformer to combine these “disjunctioned” futures, and then convert DTO’s into model classes.
Well, that is quite idiomatic functional Scala, lots of pleasure. Now let’s think about how we will export loadUser method to library users.
Share the Future
To follow the article goals we need to answer the question: what is the idiomatic way to handle asynchronous call in Javascript? I already hear experienced frontenders laughing, because there are no such thing. Callbacks, event emitters, promises, fibers, generators, async/await — all of them are somehow valid approaches. So what should we choose?
I think, the closest thing to Scala Future in Javascript are Promises. Promises are very popular and are already native in most modern browsers. So we’ll stick with them.
First, we must let our Scala code know about those promises. Until Scalajs 0.6.7 we would have to use Promise typed facade from scalajs-dom. But with Scalajs 0.6.7 things became much easier, we will just use the “standard” Promises.
All we have to do now is to convert a Future into Promise. Again, since version 0.6.7 this is not more a problem — there’s a toJSPromise converter in JSConverters . We will just need to help it with the left side of our Xor — convert it to a failed Future to get a rejected Promise:
1 2 3 4 5 6 7 8 9 10 |
object promise { implicit class JSFutureOps[R: ClassTag](f: Future[Xor[String, R]]) { def toPromise(implicit ectx: ExecutionContext): Promise[R] = f.flatMap[R] { case Xor.Right(res) => Future.successful(res) case Xor.Left(str) => Future.failed(new JavaScriptException(str)) }.toJSPromise } } |
So let’s share the promise with our JS friends! As usual, we put it to Github object, near the original method:
1 2 3 4 5 |
def loadUser(login: String): Future[String Xor User] = //... @JSExport("loadUser") def loadUserJS(login: String): Promise[User] = loadUser(login).toPromise |
Here in case of failed future we’re rejecting promise with the exception message. That’s all, we can test the whole API now:
1 2 3 4 5 6 7 8 9 |
// JS Github.loadUser("vpavkin") .then(function (result) { console.log("Name: ", result.name); }, function (error) { console.log("Error occured:", error) }); // Name: Vladimir Pavkin |
Well, we did it! We can use Futures and everything else we are got used to — and still export idiomatic JS API.
For more API usage examples see full demo.js. To play more with the project, just fetch the repo, then build and run it.
Conclusion
Putting it all together, here are some general advice on writing a Javascript library with Scala.js:
- Cache exported objects on startup.
- Export seamless types “as is”.
- Don’t export Options, Lists and other Scala standard. Put a JS-friendly getter nearby, that converts to js.UndefOr and js.Array. BTW, same with Map => js.Dictionary.
- Don’t export constructors. Use a JS-friendly factory method. JS-friendly means it accepts js.* types and converts it to Scala standard types.
- Mixin a string type property into sum types.
- Export Future s as js.Promise s
- Scala first. You are a Scala developer, so don’t limit yourself in any way: use all the power you like. You know now, that you’ll be able to export it.
Links
- Scala.js exporting and interop guides.
- Example library code: https://github.com/vpavkin/scalajs-library-tips
Scala GWT? не?
Не:) Немного устаревший ответ, но хороший:
http://stackoverflow.com/questions/18557181/scala-js-vs-scala-gwt-for-client-web-development?answertab=votes#tab-top
Если вкратце, GWT – это огромный фреймворк, а scala.js – просто способ писать под браузер на Scala. Например, ты можешь подключить scalajs-react и написать проект на Scala, а на выходе у тебя будет приложение на обычном ReactJS.
Чтобы лучше понять, надо потыкать 🙂
Ну ок 🙂