3

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?

Community
  • 1
  • 1
Matthew
  • 238
  • 2
  • 7
  • 1
    Actually, at a quick glance that looks about right. It's longer than average, but that's because you're putting a bunch of effort into strongly typing everything, and adding Scala semantics. (In particular, transforming the type of the callback -- but that's simply not a trivial thing to do, so it's not really surprising that it takes a bit of effort.) – Justin du Coeur Jul 28 '15 at 15:38
  • I agree. The only improvement I see would be to use directly `js.Array[UploadResult]` instead of `js.Array[js.Dynamic]` in the `js.Function` type. That would remove the need for `.map(_.asInstanceOf[UploadResult])` in `Cloudinary.openUploadWidget`. – sjrd Jul 28 '15 at 15:56
  • Thanks for the feedback and using js.Array[UploadResult] worked nicely. – Matthew Jul 30 '15 at 10:41
  • @Matthew Could you answer the question yourself with your solution and mark your answer as accepted? That helps future learners. – Per Wiklander Feb 10 '16 at 20:29
  • @PerWiklander Okay done. Thanks for the reminder! – Matthew Feb 11 '16 at 14:12

1 Answers1

1

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]
  }
}
Matthew
  • 238
  • 2
  • 7