Designing facades in Scala.js

Scala.js builds a bridge from the Scala world to the JavaScript world. Today Scala developers can take all the magic of Scala with many powerful libraries and use it in front-end applications. But after passing the bridge our developer suddenly might find himself in very different environment. Does he know something about transition animation, or about event system on mobile devises?
Fortunately JavaScript itself is ready to give a huge bulk of awesome libraries for all cases. All we need to do is to find an appropriate library and write a facade.
You can find this in the Scala.js documentation:
When writing an application with Scala.js, it is expected that the main application logic be written in Scala.js, and that existing JavaScript libraries are leveraged. Calling JavaScript from Scala.js is therefore the most important direction of interoperability.
Facade types are zero-overhead typed APIs for JavaScript libraries. They are similar in spirit to TypeScript type definitions.
JavaScript has dynamic type checking. If we want to have some type checking on compile time, we must use some interface that fills the role of naming types. In Scala.js we can use traits. Scala.js documentation says that the trait must inherit directly or indirectly from js.Any
(usually from js.Object
). Native JS types should be annotated with @js.native
.
OK. Let’s get down to business. Recently I had to use Sortable.js library in my Scala.js project and I wrote a facade. Now I can use this as an example.
Sortable is a minimalist JavaScript library for sortable drag-and-drop lists. I think it’s the best library of this sort. But let’s look at this library from the point of a facade builder.
It has:
- several dynamic methods
- the configuration object with a big list of options
- several static methods, placed in a special namespace
Sortable.utils
- custom events
The minimum that we should do to start using the library is to write facade for dynamic methods:
@js.native
class Sortable(element: Element, props: js.Any) extends js.Any {
val el: Element = js.native
def option(name:String):js.Dynamic = js.native
def option(name:String, value: js.Any):Unit = js.native
def closest(el:Node, selector:String):js.UndefOr[Node] = js.native
def closest(el:Node):js.UndefOr[Node] = js.native
def toArray(): js.Array[String] = js.native
def sort(order:js.Array[String]):Unit = js.native
def save(): Unit = js.native
def destroy(): Unit = js.native
Methods with same names and different signatures can be overloaded
It’s enough to make our first sortable list:
new Sortable(dom.document.getElementById("qux"), js.Dictionary(
"group" -> js.Dictionary(
"name" -> "qux",
"put" -> js.Array("foo", "bar")),
"animation" -> 100
)
)
In this javascript library Sortable
has only one static method create
. It’s a factory to create new Sortable
instance. We can use it in our Scala code:
@js.native
object Sortable extends js.Object {
@JSName("create")
def apply(element:Element):Sortable = js.native
@JSName("create")
def apply(element:Element, props:js.Any):Sortable = js.native
}
We use the companion object of our Sortable
class for static methods. Now we can omit new
:
Sortable(dom.document.getElementById("qux"), js.Dictionary(
"group" -> js.Dictionary(
"name" -> "qux",
"put" -> js.Array("foo", "bar")),
"animation" -> 100
)
)
It’s progress! But we must go further! Look at the parameters. The first parameter is a html element. The second one is a javascript object (configuration object). We used js.Dictionary
to form it. If we want type checking we need to use some technique. Scala.js 0.6.14 now allows to have concrete members in @ScalaJSDefined
traits, if their right-hand-side is = js.undefined
Let’s write a facade for our configuration object and custom events:
@ScalaJSDefined
trait EventS extends js.Object {
val `type`: js.Any
val to: js.Any
val from: js.Any
val item: js.Any
val oldIndex: js.Any
val newIndex: js.Any
}
@ScalaJSDefined //move event
trait EventM extends js.Object {
val `type`: js.Any
val to: js.Any
val from: js.Any
val dragged: js.Any
val draggedRect: js.Any
val related: js.Any
val relatedRect: js.Any
}
@ScalaJSDefined
trait SortableProps extends js.Object {
val group: js.UndefOr[String | js.Any] = js.undefined
val sort: js.UndefOr[Boolean] = js.undefined
val delay: js.UndefOr[Int] = js.undefined
val disable: js.UndefOr[Boolean] = js.undefined
val store: js.UndefOr[js.Any] = js.undefined
val animation: js.UndefOr[Int] = js.undefined
val handle: js.UndefOr[String] = js.undefined
val filter: js.UndefOr[String] = js.undefined
val draggable: js.UndefOr[String] = js.undefined
val ghostClass: js.UndefOr[String] = js.undefined
val chosenClass: js.UndefOr[String] = js.undefined
val dragClass: js.UndefOr[String] = js.undefined
val dataIdAttr: js.UndefOr[String] = js.undefined
val forceFallback: js.UndefOr[Boolean] = js.undefined
val fallbackClass: js.UndefOr[String] = js.undefined
val fallbackOnBody: js.UndefOr[Boolean] = js.undefined
val fallbackTolerance: js.UndefOr[Int] = js.undefine
val scroll: js.UndefOr[Boolean | Element] = js.undefined
val scrollFn: js.UndefOr[js.Function3[Int, Int, Event, Unit]] = js.undefined
val scrollSensitivity: js.UndefOr[Int] = js.undefined
val scrollSpeed: js.UndefOr[Int] = js.undefined
val setData: js.UndefOr[js.Function2[DataTransfer, Element,Unit]] = js.undefined
val onChoose: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
val onStart: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
val onEnd: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
val onAdd: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
val onUpdate: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
val onSort: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
val onRemove: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
val onFilter: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
val onMove: js.UndefOr[js.Function2[EventM, Event, Unit]] = js.undefined
val onClone: js.UndefOr[js.Function1[EventS, Unit]] = js.undefined
}
And now we can write this:
val get: js.Function1[Sortable, js.Array[String]] = { (sortable: Sortable) => {
import js.JSConverters._ //this adds additional 30kB :((
val order = window.localStorage.getItem(sortable.option("group").name.asInstanceOf[String])
order match {
case x:String => x.split("|").toJSArray
case _ => js.Array[String]()
}
}}
val set: js.Function1[Sortable, Unit] = { (sortable: Sortable) => {
val order = sortable.toArray()
window.localStorage.setItem("localStorage-example", order.join("|"))
}}
val storeProps = new SortableProps{
override val group = "localStorage-example"
override val store = js.Dictionary(
"get" -> get,
"set" -> set
)
override val onStart: UndefOr[Function1[EventS, Unit]] = js.defined{
(event:EventS) => console log event
}
}
Sortable(dom.document.getElementById("storage"), storeProps)
Is this technique more convenient than using js.Dictionary
? It’s up to you.
A local storage is used to store the order of a sortable list. Two functions are defined (get
and set
). The set
function takes a Sortable
object, and sets a string representation of its order to the local storage. The get
function gets this string from the local storage and returns a js.Array
. The import of js.JSConverters._
is very important. It needs for implicit conversion of Array
to js.Array
.
Look how is the property onStart
defined in this example. js.defined
explicitly upcasts an A
to a js.UndefOr[A]
.
This JavaScript library has an object Sortable.utils
with an additional set of static methods. To write a facade it’s necessary to use annotation @JSName
:
@JSName("Sortable.utils")
@js.native
object Utils extends js.Object {
def on(el:Node, event:String, fn: js.Function1[Event, Unit]):Unit = js.native
def off(el:Node, event:String, fn: js.Function1[Event, Unit]):Unit = js.native
def css(el:Node):js.Object = js.native
def css(el:Node, prop:String):js.Any = js.native
def css(el:Node, prop:String, value:String):Unit = js.native
def find(ctx:Node, tagName:String):js.Array[Node] = js.native
def find(ctx:Node, tagName:String, iterator:js.Function2[Node, Int, Unit]):js.Array[Node] = js.native
def bind(ctx:js.Any, fn:js.Function):js.Function = js.native
def is(el:Node, selector:String):Boolean = js.native
def closest(el:Node, selector:String, ctx:Node):js.UndefOr[Node] = js.native
def closest(el:Node, selector:String):js.UndefOr[Node] = js.native
def clone(el:Node):Node = js.native
def toggleClass(el:Node, name:String, state:Boolean):Unit = js.native
I hope that my post has been useful and it will help you not to be afraid of writing you own facades.
If you are interested in sortable-js-facade
you can use it by adding to your build.sbt:
libraryDependencies ++= Seq(
"org.scala-js" %%% "scalajs-dom" % "0.9.1",
"net.scalapro" %%% "sortable-js-facade" % "0.2.1"
)
jsDependencies ++= Seq(
"org.webjars.bower" % "github-com-RubaXa-Sortable" % "1.4.2"
/ "1.4.2/Sortable.js" minified "Sortable.min.js"
)
If you have questions don’t hesitate to make a comment.
If you found my post useful it would be kind of you to share it with others.
See full code on GitHub
See Demo