diff --git a/docs/public/docs/writing-components.md b/docs/public/docs/writing-components.md index d5e82e6a..e3b07fac 100644 --- a/docs/public/docs/writing-components.md +++ b/docs/public/docs/writing-components.md @@ -75,6 +75,31 @@ When defining the `Props` and `State` types, Slinky accepts **any type definitio where `props` is the `String` value passed in from a parent. +### Typed Props +If you need props with typed parameters, Slinky requires the typed props to be declared as a separate case class with the props specified as a type alias. Currently, this approach does not work with the `@react` style, so you will need to declare a `ComponentWrapper` instead. For example, a simple component with type parameters for both `Props` and `State` would look like: + +```scala +object MyComponent extends ComponentWrapper { + case class TypedProps[T](value: T) + case class TypedState[T](value: T) + + type Props = TypedProps[_] + type State = TypedState[_] + + class Def(jsProps: js.Object) extends Definition(jsProps) { + override def initialState = TypedState(props.value) + + override def render(): ReactElement = { + state.value.toString + } + } +} + +// ... + +MyComponent(MyComponent.TypedProps(123)) +``` + ## Component Styles With Slinky 0.2.0, the `@react` macro annotation was introduced to reduce the boilerplate involved with creating components. Most examples in the documentation will use the macro annotation, but it is always possible to use the no-annotation API with just a few changes. diff --git a/readWrite/src/main/scala/slinky/readwrite/GenericDeriveImpl.scala b/readWrite/src/main/scala/slinky/readwrite/GenericDeriveImpl.scala index a72a9662..aacfe098 100644 --- a/readWrite/src/main/scala/slinky/readwrite/GenericDeriveImpl.scala +++ b/readWrite/src/main/scala/slinky/readwrite/GenericDeriveImpl.scala @@ -107,7 +107,7 @@ abstract class GenericDeriveImpl(val c: whitebox.Context) { self => c.abort(c.enclosingPosition, "Cannot derive a typeclass for a type parameter") } else if (symbol.isModuleClass && c.typecheck(c.parse(symbol.asClass.module.fullName), silent = true).nonEmpty) { createModuleTypeclass(tTag.tpe, c.parse(symbol.asClass.module.fullName)) - } else if (symbol.isClass && symbol.asClass.isCaseClass) { + } else if (symbol.isClass && symbol.asClass.isCaseClass && symbol.asType.typeParams.size == tTag.tpe.typeArgs.size) { val constructor = symbol.asClass.primaryConstructor val companion = symbol.asClass.companion if (companion != NoSymbol) { diff --git a/tests/src/test/scala/slinky/core/ComponentTest.scala b/tests/src/test/scala/slinky/core/ComponentTest.scala index b19cb41a..2baf3688 100644 --- a/tests/src/test/scala/slinky/core/ComponentTest.scala +++ b/tests/src/test/scala/slinky/core/ComponentTest.scala @@ -269,6 +269,22 @@ object DefaultStateParamsComponent extends ComponentWrapper { } } +object TypeParamsComponent extends ComponentWrapper { + case class TypedProps[T](abc: T) + case class TypedState[T](abc: T) + + type Props = TypedProps[_] + type State = TypedState[_] + + class Def(jsProps: js.Object) extends Definition(jsProps) { + override def initialState = TypedState(props.abc) + + override def render(): ReactElement = { + state.abc.toString + } + } +} + class ComponentTest extends AsyncFunSuite { test("setState given function is applied") { val promise: Promise[Assertion] = Promise() @@ -444,4 +460,14 @@ class ComponentTest extends AsyncFunSuite { promise.future } + + test("Can render a component with type parameters") { + val container = dom.document.createElement("div") + ReactDOM.render( + TypeParamsComponent(TypeParamsComponent.TypedProps(123)), + container + ) + + assert(container.innerHTML == "123") + } }