Skip to Content
Course content

202: Interfacing Scala.js with JavaScript Libraries

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent more hours than I care to admit staring at TypeError: confetti is not a function in the browser console. It usually happens when you're feeling confident, you've found a great NPM package, and you've written a clean Scala facade for it—only for the runtime to tell you that your facade is a lie.

The "Not a Function" Nightmare

Let's say you want to add a bit of polish to your app using canvas-confetti. It's a simple library: you call a function, and confetti flies. You'd probably start by writing a facade like this:

import scala.scalajs.js
import scala.scalajs.js.annotation._

@JSImport("canvas-confetti", "confetti")
external val confetti: js.JSFunction1[js.Any, js.Any] = confetti

You compile, you run, and you call confetti(js.Dynamic.literal()). Immediately, the console screams that confetti is undefined. You check the NPM package, and yes, the function is there. You check your import string, and it matches the package name. So why is it broken?

The issue is a fundamental mismatch between how JavaScript handles Named Exports versus Default Exports. When you write @JSImport("canvas-confetti", "confetti"), you are telling Scala.js to look for a specific export named confetti inside the module. But canvas-confetti uses export default. In the eyes of the JavaScript module system, the "name" of that export isn't confetti—it's default.

Mapping Default Exports Correctly

To fix this, you have to be explicit about the default export. In Scala.js, the default export is literally named "default" in the import annotation. Here is the correction:

@JSImport("canvas-confetti", "default")
external val confetti: js.JSFunction1[js.Any, js.Any] = confetti

Now, when the JS engine loads the module, Scala.js grabs the default property of the module object, which happens to be the confetti function we want. If you're ever unsure whether a library uses a default or named export, I highly recommend peekng at the package.json or the source code on GitHub. If you see module.exports = ... or export default ..., you need to use "default".

Moving Beyond js.Any with @JSNative

While js.Any gets the code running, it's a bit lazy. We're using Scala because we like types, so passing js.Any into a confetti configuration object feels wrong. I don't want to rely on js.Dynamic.literal() every time I call a function; it's error-prone and lacks autocomplete.

The "professional" way to handle JS options is to define a @JSNative trait. This allows you to define the shape of the JavaScript object you expect to pass, providing type safety on the Scala side without adding any runtime overhead.

@JSNative
trait ConfettiOptions extends js.Object {
  val particleCount: js.Int
  val spread: js.Int
  val origin: js.Object // You could nest another @JSNative trait here for x, y coordinates
}

@JSImport("canvas-confetti", "default")
external val confetti: js.JSFunction1[ConfettiOptions.type, js.Any] = confetti

// Now we can create a typed options object
val options = js.Dynamic.literal(
  particleCount = 150,
  spread = 70
).asInstanceOf[ConfettiOptions.type]

confetti(options)

By using a @JSNative trait, you've created a contract. If you accidentally try to pass a String where the JS library expects an Int, the Scala compiler might not catch it (since it's just a facade), but your code is significantly more readable. It documents exactly what the JS library expects without you having to keep the documentation tab open in your browser 24/7.




📋 Practical Task

Integrating the Chart.js Library for Data Visualization

Your task is to create a Scala.js facade for the chart.js library to render a simple bar chart. You will need to handle both the main constructor and the configuration object.

  • Requirement 1: Define a @JSImport for the Chart class. Note that Chart.js uses a named export called Chart.
  • Requirement 2: Create a @JSNative trait called ChartConfig that includes at least the type (e.g., "bar") and data properties.
  • Requirement 3: Write a Scala function that initializes a new Chart instance, passing in a canvas element from the DOM and your typed ChartConfig object.
  • Requirement 4: Ensure the data property in your config is properly structured to handle labels and datasets using js.Dynamic.literal or further @JSNative traits.

Hint: Remember that the Chart constructor expects the canvas element as the first argument and the configuration object as the second.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.