shadaj / slinky

Write Scala.js React apps just like you would in ES6
https://slinky.dev
MIT License
652 stars 57 forks source link

Typed external component #330

Open joan38 opened 4 years ago

joan38 commented 4 years ago

Given the following working code:

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import scala.scalajs.js.{|, undefined, UndefOr}
import slinky.core.{ExternalComponentWithAttributes, TagMod}
import slinky.core.annotations.react
import slinky.web.html._

@react object Autocomplete extends ExternalComponentWithAttributes[*.tag.type] {
  @JSImport("@material-ui/lab/Autocomplete", JSImport.Default) @js.native
  private object Autocomplete extends js.Object

  case class Props(
      options: Seq[String],
      renderInput: js.Object with js.Dynamic => TagMod[*.tag.type],
      value: UndefOr[String | Seq[String]] = undefined,
      onChange: (js.Object, String) => Unit = (_, _) => (),
      disabled: Boolean = false,
      loading: Boolean = false,
      disableClearable: Boolean = false,
      autoHighlight: Boolean = false,
      loadingText: String = "Loading..."
  )

  override val component: String | js.Object = Autocomplete
}

I'd like to make have generic typed props as the non working following code:

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import scala.scalajs.js.{|, undefined, UndefOr}
import slinky.core.{ExternalComponentWithAttributes, TagMod}
import slinky.core.annotations.react
import slinky.web.html._

@react object Autocomplete extends ExternalComponentWithAttributes[*.tag.type] {
  @JSImport("@material-ui/lab/Autocomplete", JSImport.Default) @js.native
  private object Autocomplete extends js.Object

  case class Props[T](
      options: Seq[T],
      renderInput: js.Object with js.Dynamic => TagMod[*.tag.type],
      value: UndefOr[T | Seq[T]] = undefined,
      onChange: (js.Object, T) => Unit = (_, _) => (),
      disabled: Boolean = false,
      loading: Boolean = false,
      disableClearable: Boolean = false,
      autoHighlight: Boolean = false,
      loadingText: String = "Loading..."
  )

  override val component: String | js.Object = Autocomplete
}

Unless I'm missing something this is not currently supported or not documented. Is there a workaround?

Thanks

evbo commented 3 years ago

this is how I create typed slinky components, could something similar work for external?:

// here's how I type my components when creating them:
div(
  // this could be instantiated once in a singleton object alternatively...
  new NeedTyping[SubMyType]().MyComponent(thing = someTypedThing)
)
// where the component is defined like:

// or can be case class too
class NeedTyping[T <: MyType] extends AndCanDoHigherOrderToo[T] {
  @react object MyComponent {
    case class Props(thing: T)
    val component = FunctionalComponent[Props] { props =>
        // do stuff...
    }
  }
}

Works for me ;)

joan38 commented 3 years ago

That a workaround :) Thanks for sharing!

evbo commented 3 years ago

@joan38 did it work for you? One issue I noticed is if my components have callbacks where the javascript sends generic arguments, I'd get warnings such as the following and it wouldn't work quite right at runtime:

Using fallback derivation for type T => Unit (derivation: MacroWritersImpl)

joan38 commented 3 years ago

I did not try. But I don't think I'll try anytime soon since I have other higher priority stuff on the fire.

shadaj commented 3 years ago

@evbo you should be able to resolve this by requiring typeclass instances at the NeedTyping level, so your definition would become something like: class NeedTyping[T <: MyType : Reader: Writer]

evbo commented 2 years ago

and just for the uninitiated, class NeedTyping[T <: MyType : Reader: Writer] is called a context bound and is shorthand for adding implicit value arguments to the class: class NeedTyping[T <: MyType](implicit r: Reader[T], w: Writer[T])

evbo commented 1 year ago

One extremely painful side effect of this approach is each time you instantiate the class it will cause React to Remount. I had an input defined inside a component that kept losing state every render. This turned out to be the reason why and it was very hard to catch.

So exercise extreme caution: do not instantiate inside a render! If you're using hooks, always instantiate outside your val component definition!