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?

5
  • 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.) Commented Jul 28, 2015 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. Commented Jul 28, 2015 at 15:56
  • Thanks for the feedback and using js.Array[UploadResult] worked nicely. Commented Jul 30, 2015 at 10:41
  • @Matthew Could you answer the question yourself with your solution and mark your answer as accepted? That helps future learners. Commented Feb 10, 2016 at 20:29
  • @PerWiklander Okay done. Thanks for the reminder! Commented Feb 11, 2016 at 14:12

1 Answer 1

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]
  }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.