I am trying to write a typed façade for my library Paths.js, following the official guide.
What I would like to be able to translate is a call like the following:
var Polygon = require('paths-js/Polygon');
var polygon = Polygon({
points: [[1, 3], [2, 5], [3, 4], [2, 0]],
closed: true
});
into
val polygon = Polygon(points = List((1, 3), (2, 5), (5, 6)), closed = true)
but I am not sure what I need to do to get to this point.
What I have done is something like the following
type Point = (Number, Number)
trait PolygonOpt {
val points: Array[Point]
val closed: Boolean
}
@JSName("paths.Polygon")
object Polygon extends js.Object {
def apply(options: PolygonOpt): Shape = js.native
}
Then, I can invoke it like
class Opt extends PolygonOpt {
val points: Array[Point] = Array((1, 2), (3, 4), (5, 6))
val closed = true
}
val opts = new Opt
val poly = Polygon(opts)
I have a few doubts on this:
- I am at a point where everything compiles, but the resulting javascript fails at the point of invocation. I believe that this is because I am passing an instance of
PolygonOpt
, while the runtime expects a javascript object literal
- is the definition of
Point
translated into a js array with two components?
- I would like to be able to overload
Polygon.apply
like def apply(points: Seq[Point], closed: Boolean): Shape
, but scala.js does not let me write method implementation inside Polygon
since it extends js.Object
Moreover, I have both a version of my library using common.js (which is split into several files, one for each component) and another one which can be used as a single <script>
tag, putting everything under the namespace paths
(which is the one I am using right now).
Which one works better for a Scala.js wrapper?
First, make sure to read the JS interop doc, including the calling JavaScript guide. I think you did so already, because you already have something reasonable. But you should pay particular attention to the parts that say that Scala types and JavaScript types are completely unrelated unless explicitly mentioned.
So, an Int
is a proper JS number
(in the range of an int). But an Array[Point]
has nothing to do with a JavaScript array. A Tuple2
(such as (1, 3)
) even less so. So:
Is the definition of Point
translated into a js array with two components?
No, it is not. As such, it is completely incomprehensible to JavaScript. It is opaque.
Worse, a PolygonOpt
, since it does not extend js.Object
, is completely opaque as well from JavaScript, which explains why you cannot see the fields points
and closed
.
The first thing to do is to type accurately, with JavaScript-understandable types (extending js.Object
), your JS API. In this case, it would look like this:
type JSPoint = js.Array[Int] // or Double
trait PolygonOpts extends js.Object {
val points: js.Array[JSPoint] = js.native
val closed: Boolean = js.native
}
@JSName("paths.Polygon")
object Polygon extends js.Object {
def apply(options: PolygonOpt): Shape = js.native
}
Now, the thing is that it is not really easy to create an instance of PolygonOpts
. For this refer to this SO question:
object PolygonOpts {
def apply(points: js.Array[JSPoint], closed: Boolean): PolygonOpts = {
js.Dynamic.literal(
points = points,
closed = closed
).asInstanceOf[PolygonOpts]
}
}
Finally, you can expose the Scala-esque API you wanted in the first place with implicit extensions:
import js.JSConverters._
object PolygonImplicits {
implicit class PolygonObjOps(val self: Polygon.type) extends AnyVal {
def apply(points: List[(Int, Int)], closed: Boolean): Shape = {
val jsPoints =
for ((x, y) <- points.toJSArray)
yield js.Array(x, y)
Polygon(PolygonOpts(jsPoints, closed))
}
}
}
Implicit extensions are the way to write Scala methods that appear to be available on objects extending js.Object
, since, as you discovered, you cannot actually implement methods inside a js.Object
.