10

In idiomatic JavaScript it's common to have a function accept an "options object" as the last parameter. This is where you'd usually put all the option/seldom-used parameters, e.g.

jQuery.ajax({
   url: "http://www.example.com/foo",
   success: function() {
      ..
   }
})

The current documentation for Scala.JS recommands using a Scala trait to represent the options object, but that leads to a problem when you have to create the options since you cannot pass an anonymous class into JavaScript code.

How can such option objects be created from Scala code?

Adowrath
  • 701
  • 11
  • 24
  • possible duplicate of [What is the suggested way to instantiate a js.Object for API wrappers](http://stackoverflow.com/questions/23663059/what-is-the-suggested-way-to-instantiate-a-js-object-for-api-wrappers) – gzm0 Oct 30 '14 at 07:07

3 Answers3

12

We recommend the following (if you chose to create facade-types):

trait AjaxOptions extends js.Object {
  val url: String = js.native
  val success: js.Function0[Unit] = js.native
}

object AjaxOptions {
  def apply(url: String, success: js.Function0[Unit]): AjaxOptions = {
    js.Dynamic.literal(url = url, success = success).asInstanceOf[AjaxOptions]
  }
}

The advantage is that the type-unsafe cast is contained in a single location. Further, if you ever decide to add/remove fields to/from AjaxOptions, you will (hopefully) think of also adapting the companion's apply method. As a result, the typer will inform you where you have to change your invocations (rather than just having the new field set to undefined).

Please refer to What is the suggested way to instantiate a js.Object for API wrappers for more.

Adowrath
  • 701
  • 11
  • 24
gzm0
  • 14,752
  • 1
  • 36
  • 64
4

Since Scala.js has evolved, I'm amending my answer with the current best practice:

At this point, you should use a trait to describe the options object, like this:

trait AjaxOptions extends js.Object {
  var url: String
  var success: UndefOr[js.Function0[Unit]] = js.undefined
}

That UndefOr[T] means "this field might contain T, or might be undefined"; note that you are initializing those to js.undefined, so they have a default value.

Then, at the call site, you simply override the values that you need to set:

val ajaxResult = new AjaxOptions {
  url = "http://www.example.com/foo"
}

Note the curly braces: you're actually creating an anonymous subclass of AjaxOptions here, that does what you want. Any fields you don't override (such as success above) get left as undefined, so the library will use its default.

Old Answer:

This question is pretty old, but since people are still coming here:

If you have a large and complex options object (as is typical of, say, jQuery UI classes), you may want to build a facade for that using JSOptionBuilder, which is found in the jsext library. JSOptionBuilder isn't quite a panacea, but it's a not-too-much boilerplate mechanism for constructing and using arbitrarily complex options objects.

Justin du Coeur
  • 2,699
  • 1
  • 14
  • 19
2

Here's a method that I've found to work quite well:

val fooOptions = js.Dynamic.literal().asInstanceOf[FooOptions]
fooOptions.url = ...
fooOptions.bar = ...
jsFunc(..., fooOptions)

Of course this assumes that the FooOptions trait has declared the fields as var. If not, you'll have to use

val fooOptions = js.Dynamic.literal(
   url = ...,
   bar = ...,
)
jsFunc(..., fooOptions)

but that is less type-safe.

If you're declaring your own options trait, you could also add a companion object with an appropriate apply method:

trait FooOptions extends Js.Object {
   var foo: js.String = ???
   var bar: js.String = ???
}

object FooOptions {
   def apply(): FooOptions =
      js.Dynamic.literal().asInstanceOf[FooOptions]
}

That'll make calling code a lot prettier:

val fooOptions = FooOptions()
fooOptions.foo = ...
fooOptions.bar = ...
jsFunc(..., fooOptions)
  • I would not recommend doing this. If you'll ever add a field to `FooOptions`, the typer will not complain in locations where you create them. Therefore, the field will be set to `undefined`, which is type-unsound for most field types. – gzm0 Oct 30 '14 at 07:08
  • That's an interesting point, but I'm not sure I understand why Scala.Js would actually set fields that you're not assigning to. I guess I'll have to do a little test to see what the actual objects look like. – Bardur Arantsson Oct 30 '14 at 17:18
  • So, I just did a little test and it appears that my code *does* do the right thing. The resulting options object does *not* have any properties which you do not explicitly set on it from the Scala side of things. However, your version does have the advantage that it can work using "val" rather than "var", so there's that potential tradeoff. – Bardur Arantsson Oct 30 '14 at 17:39
  • 1
    I think I need to clarify my comment: Your solution **does** work. However, if you ever add a field to `FooOptions` later on (I mean to the trait), in all locations where you create an instance of `FooOptions` you have to remember **yourself** to set the additional field. If you don't, Scala.js will **not** create the field (as you said). Therefore, if someone tries to read it, it will return `undefined` (which is not a valid value). Hence it is better to abstract this unsafety away in a single method (`apply`). – gzm0 Oct 31 '14 at 11:57
  • I'm /still/ not quite sure what you mean then. When you say "someone tries to read it", do you mean if some *Scala* code tries to read it? – Bardur Arantsson Oct 31 '14 at 17:57
  • Exactly. If some Scala code tries to read that var, it will get a ClassCastException (if compliant asInstanceOf's are enabled). – gzm0 Nov 03 '14 at 07:43
  • I *see*... then I agree that your solution is better, so I've marked your answers as the accepted one instead of mine. Even if it's a litte more boilerplate is has the additional advantage of "val" over "var". – Bardur Arantsson Nov 04 '14 at 19:51