Matthew
Matthew

Reputation: 238

How to properly type a javascript callback function in scala-js when creating a facade

For many javascript libraries with async operations you pass a callback function. I've read this SO question, this one too, and read the docs but am still a bit confused as to how to properly type a callback function in scala-js when creating a facade. I am writing a facade for Cloudinary's upload widget and it has an openUploadWidget method that takes options and a callback like the following example from their docs:

cloudinary.openUploadWidget(
  { cloud_name: 'demo', upload_preset: 'a5vxnzbp'}, 
  function(error, result) { console.log(error, result) });

This is what I implemented so far in my scala-js facade:

object Cloudinary {
  def openUploadWidget(
      options: WidgetOptions,
      callback: (Either[String, Seq[UploadResult]]) => Unit): Unit = {
    _Cloudinary.openUploadWidget(
        options, 
        (error: String, results: js.Array[js.Dynamic]) => {
            callback(Option(results)
                .filterNot(_.isEmpty)
                .map(_.toSeq.map(_.asInstanceOf[UploadResult]))
                .toRight(error))
        })
  }    
}    

@JSName("cloudinary")
object _Cloudinary extends js.Object {
  def openUploadWidget(
      options: WidgetOptions,
      callback: js.Function2[String, js.Array[js.Dynamic], _]): Unit = js.native
}

trait WidgetOptions extends js.Object {
  @JSName("cloud_name") val cloudName: String = js.native
  @JSName("upload_preset") val uploadPreset: String = js.native
}

object WidgetOptions {
  def apply(cloudName: String, uploadPreset: String): WidgetOptions = {
    js.Dynamic.literal(
      cloud_name = cloudName, 
      upload_preset = uploadPreset).asInstanceOf[WidgetOptions]
}

trait UploadResult extends js.Object {
  @JSName("public_id") val publicId: String = js.native
  @JSName("secure_url") val secureUrl: String = js.native
}

And you would use it like:

def callback(results: Either[String, Seq[UploadResult]]): Unit = {}

def show(): Unit = {
  Cloudinary.openUploadWidget(
      WidgetOptions(
          cloudName = "demo",
          uploadPreset = "a5vxnzbp"),
      callback _)
}

I implemented a small wrapper to translate from the javascript callback args into something more Scala-ish because I couldn't figure out how to type the callback in a more direct fashion. This isn't bad, IMHO, but I have a sneaking suspicion that I'm not understanding something and it could be done a lot better.

Any help/suggestions?

Upvotes: 3

Views: 1120

Answers (1)

Matthew
Matthew

Reputation: 238

Thanks to Per Wiklander for reminding to follow up with this. The following code is what I settled on after implementing the suggestions and upgrading to Scala.js 0.6.6

import scala.scalajs.js
import scala.scalajs.js.annotation.JSName

object Cloudinary {
  type CloudinaryCallback = (Either[String, Seq[UploadResult]]) => Unit

  def openUploadWidget(
      options: WidgetOptions,
      callback: CloudinaryCallback): Unit = {
    _Cloudinary.openUploadWidget(options, (error: js.Dynamic, results: js.UndefOr[js.Array[UploadResult]]) => {
      callback(results
          .filterNot(_.isEmpty)
          .map(_.toSeq)
          .toRight(error.toString))
    })
  }
}

@js.native
@JSName("cloudinary")
object _Cloudinary extends js.Object {
  def openUploadWidget(
      options: WidgetOptions,
      callback: js.Function2[js.Dynamic, js.UndefOr[js.Array[UploadResult]], _]): Unit = js.native
}

@js.native
trait UploadResult extends js.Object {
  @JSName("public_id") val publicId: String = js.native
  @JSName("secure_url") val secureUrl: String = js.native
  @JSName("thumbnail_url") val thumbnailUrl: String = js.native
  @JSName("resource_name") val resourceName: String = js.native

  val `type`: String = js.native
  val path: String = js.native
  val url: String = js.native
  val version: Long = js.native
  val width: Int = js.native
  val signature: String = js.native
}

@js.native
trait WidgetOptions extends js.Object {
  @JSName("cloud_name") val cloudName: String = js.native
  @JSName("upload_preset") val uploadPreset: String = js.native
  @JSName("show_powered_by") val showPoweredBy: Boolean = js.native
  @JSName("cropping_default_selection_ratio") val croppingDefaultSelectionRatio: Double = js.native

  val sources: Array[String] = js.native
  val multiple: Boolean = js.native
  val cropping: String = js.native
  val theme: String = js.native
  val text: Map[String, String] = js.native
}

object WidgetOptions {
  def apply(cloudName: String, uploadPreset: String): WidgetOptions = {
    val map: Map[String, js.Any] = Map(
        "sources.local.title" -> "Local Files",
        "sources.local.drop_file" -> "Drop credit card image here",
        "sources.local.select_file" -> "Select File")

    js.Dynamic.literal(
        cloud_name = cloudName,
        upload_preset = uploadPreset,
        sources = js.Array("local"),
        multiple = false,
        cropping = "server",
        theme = "minimal",
        show_powered_by = false,
        cropping_default_selection_ratio = 1.0d,
        text = js.Dynamic.literal.applyDynamic("apply")(map.toSeq: _*)).asInstanceOf[WidgetOptions]
  }
}

Upvotes: 1

Related Questions