A boilerplate-free way to make React components in the wild usable in scalajs-react apps. Write an object for each component, use one of the four provided macros, and start using it in a type-safe manner in scalajs-react apps.
For example, to create a component corresponding to react-tagsinput, define a class as follows:
object TagsInput extends ReactBridgeComponent {
def apply(defaultValue: js.UndefOr[Seq[String]] = js.undefined,
value: js.UndefOr[Seq[String]] = js.undefined,
placeholder: js.UndefOr[String] = js.undefined,
onChange: js.UndefOr[js.Array[String] => Callback] = js.undefined,
validate: js.UndefOr[String => CallbackTo[Boolean]] = js.undefined,
transform: js.UndefOr[String => CallbackTo[String]] = js.undefined): WithPropsNoChildren = autoNoChildren
}
Then use it in a scalajs-react app the same way as any other component.
div(
TagsInput(value = Seq("foo","bar"), onChange = printSequence _)
)
If you want to pass DOM attributes as well as React special attributes such as "key" as additional properties, you can easily do so as follows:
div(
TagsInput(value = Seq("foo","bar"), onChange = printSequence _)(className := "tags", key := "key-1")
)
Finally, while TagsInput
doesn't allow children (as signified by the return type of the method), if it were to, you could pass children as follows:
div(
TagsInput(value = Seq("foo","bar"), onChange = printSequence _)(className := "tags", key := "key-1")(
"child1",
div(className := "some-div")(
span(className: "some-span")("content")
)
)
)
- Add the Sonatype snapshots resolver to your SBT configuration:
resolvers += Resolver.sonatypeRepo("snapshots")
- Add the following dependency to your scalajs-react project:
libraryDependencies += "com.payalabs" %%% "scalajs-react-bridge" % "0.4.0-SNAPSHOT"
The core logic of bridging the JS React component to scala-react is implemented using the ReactBridgeComponent
class and four macros in it that you can use as an implementation of an apply
method (stricly speaking, you could use any name for the method, but then the component usage won't look as natural). The macro you will use depends on if the component allows children and if the component accepts arbitrary DOM attributes (TagsMod
s).
Can have children | Cannot have children | |
---|---|---|
Can take DOM attrs | auto |
autoNoChildren |
Cannot take DOM attrs | autoNoTagMods |
autoNoTagModsNoChildren |
Each of the macros return type that signify what has been already processed (and thus cannot process it again).
auto
:WithProps
(properties have been consumed, thus can passTagMod
s followed by children)autoNoChildren
:WithPropsNoChildren
(properties have been consumed, thus can passTagMod
s, but that cannot be followed by children)autoNoTagMods
:WithPropsAndTags
(properties have been consumed as areTagMod
s, thus can be followed by children)autoNoTagModsNoChildren
:WithPropsAndTagsNoChildren
(properties, tags, and children have been consumed)
ReactBridgeComponent
offers an easy way to bridge a component when an object extending it follows these conventions:
- The object name matches the function name exposed for the underlying component. For example, if the component object is declared as
object MyComponent extends ReactBridgeComponent { ... }
, the correspodingMyComponent
is available in the global space. - The object has any number of
apply
methods taking properties as arguments. Each apply method may be implemented as eitherautoConstruct
orautoConstructNoChildren
. The default property transformation assumes that each method parameter type maps to the underlying component's expected property type and the parameter name match the underlying components property name. For example, if the underlying component expects a string property with namefoo
, then the parameter type must beString
and parameter name must befoo
. The bridge automatically translates (through implicit converters) parameters withSeq
type (or its subtypes) to js array andMap
types withString
key type to js literal. You may provide custom conversions for your own types by introducing an implicit value of the JsWriter type.
If a component cannot follow the expected conventions, it can override them as following:
- If the class name doesn't match the function name, it can override
componentName
supply a different name. - If the function isn't exposed in the global space, it can override
componentNamespace
to supply the path to the function. For example, if the component function is exposed asfoo.bar.MyComponent
, you can overridecomponentNamespace
to returnfoo.bar
. - If overriding
componentName
and/orcomponentNamespace
isn't sufficient, you may overridejsComponent
to supply the component function. - If an apply method's parameters require transformation beyond what is implemented by the macros, don't use the macros as their implementation. Instead, supply your own implementation, which may still use the
jsComponent
after transforming the method parameters appropriately.
Oftentimes, React components allow adding any DOM attributes in addition to properties specific to that component. By default, bridged components allow passing any DOM attributes (as TagMod
s). Behind the scene, these attributes are merged with specific propeties passed. If you don't want this behavior, you can use the appropriate varation of macro described in the table earlier.
Oftentimes, especially with components that simply enhance a regular DOM element such as <input> don't need any special properties beyond what can be passed as DOM attributes. To handle those cases, scalajs-react-bridge
offers ReactBridgeComponentNoSpecialProps
(which extends ReactBridgeComponent
). You can extend this class without implementing anything else thus making it a one-liner.
object Button extends ReactBridgeComponentNoSpecialProps
which then may be passed any DOM attributes (as TagMod):
Button(onClick --> handleClick)("Simple Button")
As a further special case, certain components may not take any children, either. Those components may extend ReactBridgeComponentNoSpecialPropsNoChildren
without implemented anything else thus making it a one-liner.
object Input extends ReactBridgeComponentNoPropsNoChildren
which then may be used as
Input(value := currentValue, onChange ==> handleChange)