BenjaminJackman BenjaminJackman - 3 months ago 11
Scala Question

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

For the following javascript API wrapper:

@JSName("React")
object React extends js.Object {
def createClass(init: ReactClassInit): ReactClassBuilder = ???
}


What is the suggested what to instantiate the following trait

trait ReactClassInit extends js.Object {
val render: js.ThisFunction0[js.Dynamic, js.Any]
}


Currently I am doing the following:

val * = js.Dynamic.literal
val init = *(render = new js.ThisFunction0[js.Dynamic, js.Any] {
override def apply(thisArg: js.Dynamic): js.Any = {
React.DOM.div(null, "Hello ", thisArg.props.name)
}
}).asInstanceOf[ReactClassInit]
val HelloMessage = React.createClass(init)


What I don't like about this approach is that there is no type-safety
ensuring that ReactClassInit is instantiated properly.

(Here is all of the code to put things into a better context)

//Work in progress React wrapers
@JSName("React")
object React extends js.Object {
def createClass(init: ReactClassInit): ReactClassBuilder = ???
def renderComponent(cls: ReactClassInstance, mountNode: HTMLElement) = ???
val DOM: ReactDOM = ???
}

trait ReactDOM extends js.Object {
def div(huh: js.Any, something: js.String, propsOrWhat: js.Any) = ???
}

trait ReactClassInstance extends js.Object

trait ReactClassBuilder extends js.Object {
def apply(args: js.Object): ReactClassInstance
}

trait ReactClassInit extends js.Object {
val render: js.ThisFunction0[js.Dynamic, js.Any]
}



@JSExport
object ReactTodo {
//Some helpers I use.
val * = js.Dynamic.literal

@JSExport
def main() {
helloJonT()
}


//Ideal Typed Example
def helloJonT() {
val init = *(render = new js.ThisFunction0[js.Dynamic, js.Any] {
override def apply(thisArg: js.Dynamic): js.Any = {
React.DOM.div(null, "Hello ", thisArg.props.name)
}
}).asInstanceOf[ReactClassInit]
val HelloMessage = React.createClass(init)
React.renderComponent(HelloMessage(*(name = "Jon").asInstanceOf[js.Object]), document.getElementById("content"))
}

}

Answer

Currently, the recommended approach is very close to what you are doing, except that the use of js.Dynamic.literal should be encapsulated in the companion object of your trait (ReactClassInit in your case). You can provide a type-safe apply method in that companion object like this:

trait ReactClassInit extends js.Object {
  val render: js.ThisFunction0[js.Dynamic, js.Any]
}
object ReactClassInit {
  def apply(render: js.ThisFunction0[js.Dynamic, js.Any]): ReactClassInit = {
    js.Dynamic.literal(
      render = render
    ).asInstanceOf[ReactClassInit]
  }
}

which you can then use with:

val init = ReactClassInit(render = { (thisArg: js.Dynamic) =>
  React.DOM.div(null, "Hello ", thisArg.props.name)
})

Of course this is still globally unsafe. But there is only one point in your code where you use a cast, and more importantly it is close to the definition of the type. So it is more likely that if you update one, you will update the other.

I know this is not a completely satisfactory solution. But so far in our design of Scala.js we have not yet found a really good solution to this problem.

Two side notes:

1) I strongly advise against using new js.ThisFunctionN { def apply }! It is an accident that this notation works at all. Simply use a lambda like I showed in my example. If the target type is typed as a js.ThisFunctionN already (like in my code), it'll work just like that. If, as was the case in your code, the target type is js.Any (or Any), you'll need to ascribe your lambda with : js.ThisFunction (without digit) to make sure that the compiler treats it as a this-function and not a (non-this-)function, but that's all. To make it clearer, here is how it would have looked with your code:

val init = *(render = { (thisArg: js.Dynamic) =>
  React.DOM.div(null, "Hello ", thisArg.props.name)
}: js.ThisFunction).asInstanceOf[ReactClassInit]

2) You probably want your function to be typed as returning Any (or _) instead of js.Any:

trait ReactClassInit extends js.Object {
  val render: js.ThisFunction0[js.Dynamic, Any]
}

Typically when you use js.Any in the result type of js.(This)Function, you mean any value, not any JS value. And Scala's type inference works best with Any in that location.