Simple Facade — a macro-free library for zero-boilerplate scalajs-react facades, plus facades for several React components built with it.
Cross-published for Scala 2.13 and Scala 3.
resolvers += Resolver.jcenterRepo
// Facade for @material-ui/core version 4.12.3
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "material-ui-core_4" % "0.16.0"
// Facade for @material-ui/lab version 4.0.0-alpha.60
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "material-ui-lab_4" % "0.16.0"
// Facade for react-autocomplete version 1.8.1
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "react-autocomplete_1" % "0.16.0"
// Facade for react-datepicker version 4.6.0
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "react-datepicker_4" % "0.16.0"
// Facade for react-input-mask version 2.0.4
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "react-input-mask_2" % "0.16.0"
// Facade for react-phone-number-input version 3.1.44
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "react-phone-number-input_3" % "0.16.0"
// Facade for react-select version 5.2.1
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "react-select_5" % "0.16.0"
// Facade for react-waypoint version 10.1.0
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "react-waypoint_10" % "0.16.0"
// Facade for react-widgets version 5.5.1
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "react-widgets_5" % "0.16.0"
// Library for react component facades that are simple to write and simple to use
libraryDependencies += "io.github.nafg.scalajs-facades" %%% "simplefacade" % "0.16.0"A better way to define and use Scala.js facades for React components.
Existing approaches suffer from some issues:
- Require dealing with low-level JavaScript interop types like
js.UndefOrand union types - Pass all props, necessitating everything to be optional
- Reliance on macros
- Tedious to define or to use
Simple Facade solves all of these. Props are set with a concise _.propName := value syntax, only the props you set are passed, no macros are involved, and defining a new facade is just a few lines.
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import japgolly.scalajs.react.vdom.VdomNode
import japgolly.scalajs.react.vdom.html_<^._
import io.github.nafg.simplefacade.{FacadeModule, PropTypes}
import io.github.nafg.simplefacade.Implicits.vdomNodeWriter
object Badge extends FacadeModule.NodeChildren.Simple {
@JSImport("@mui/material/Badge", JSImport.Default)
@js.native object raw extends js.Object
class Props extends PropTypes.WithChildren[VdomNode] {
val badgeContent = of[VdomNode]
val children = of[VdomNode]
val color = of[String]
val variant = of[String]
}
override def mkProps = new Props
}
// Usage — settings then children:
Badge(_.badgeContent := "3", _.color := "secondary")(
<.span("mail")
)| Your component... | Extend |
|---|---|
| Has no children | FacadeModule.Simple |
Has VdomNode children (varargs) |
FacadeModule.NodeChildren.Simple |
Has VdomNode children (keyed array) |
FacadeModule.ArrayChildren.Simple |
Has typed children of type C |
FacadeModule.ChildrenOf.Simple[C] |
Has props parametric in a type A |
FacadeModuleP |
Props are set using the _.propName := value lambda syntax:
MyComponent(_.color := "red", _.size := 42)Under the hood, _.color := "red" is a function Props => Setting — the _ is the Props instance and := creates a Setting that writes the value to the JS props object.
Available operators:
| Operator | Description |
|---|---|
:= |
Set a prop to a value |
:=? |
Set from an Option — None passes undefined |
.setAs[B] |
Set a prop using a different type's Writer |
.setRaw |
Set a prop to a raw js.Any, bypassing the Writer |
Boolean shorthand: A boolean prop can be set by just listing it (no := true needed):
MyComponent(_.isClearable, _.showYearDropdown)
// equivalent to:
MyComponent(_.isClearable := true, _.showYearDropdown := true)Dynamic props for prop names not defined in the Props class:
MyComponent(_.dyn.`aria-label`("Close"), _.dyn.tabIndex(0))When the same prop key is set more than once, Simple Facade merges values intelligently rather than using last-value-wins:
className— concatenated with a space ("btn" + " " + "btn-primary")style— shallow-merged viaObject.assignon*event handlers — chained so both fire in sequencechildrenarrays — concatenated- Everything else — last value wins
This means you can layer settings without worrying about overwriting:
val defaults: Seq[MyComponent.Setting] = MyComponent.Settings(
_.className := "base-style",
_.variant := "outlined"
)
// Later, add more settings — className values are merged:
MyComponent(defaults, _.className := "extra-style")
// renders with className="base-style extra-style"import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import io.github.nafg.simplefacade.{FacadeModule, PropTypes}
object ReactInputMask extends FacadeModule.Simple {
@JSImport("react-input-mask", JSImport.Default)
@js.native object raw extends js.Object
class Props extends PropTypes {
val mask = of[String]
val maskChar = of[String]
val alwaysShowMask = of[Boolean]
}
override def mkProps = new Props
}
// Usage:
ReactInputMask(_.mask := "99/99/9999", _.alwaysShowMask)import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import japgolly.scalajs.react.vdom.VdomNode
import japgolly.scalajs.react.vdom.html_<^._
import io.github.nafg.simplefacade.{FacadeModule, PropTypes}
import io.github.nafg.simplefacade.Implicits.vdomNodeWriter
object MyCard extends FacadeModule.NodeChildren.Simple {
@JSImport("my-card", JSImport.Default)
@js.native object raw extends js.Object
class Props extends PropTypes.WithChildren[VdomNode] {
val children = of[VdomNode]
val elevated = of[Boolean]
val title = of[String]
}
override def mkProps = new Props
}
// Usage — settings, then children:
MyCard(_.title := "Hello", _.elevated)(
<.p("Card content"),
<.p("More content")
)
// No children — the component renders as a VdomElement directly:
val element: VdomElement = MyCard(_.title := "Empty card")For frequently-used props, you can add convenience overloads:
import java.time.LocalDate
import japgolly.scalajs.react.Callback
import japgolly.scalajs.react.extra.StateSnapshot
import io.github.nafg.simplefacade.{FacadeModule, Factory}
object DatePicker extends FacadeModule {
// ... raw, Props, mkProps as usual ...
def apply(
selected: Option[LocalDate]
)(onChange: Option[LocalDate] => Callback): Factory[Props] =
factory(_.selected := selected, _.onChange := onChange)
def apply(snapshot: StateSnapshot[Option[LocalDate]]): Factory[Props] =
apply(snapshot.value)(snapshot.setState)
}For components generic over a type (e.g., a select component):
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import japgolly.scalajs.react.Callback
import io.github.nafg.simplefacade.{FacadeModuleP, HasOpaqueReaderWriter, PropTypes}
import io.github.nafg.simplefacade.Implicits.callbackToWriter
object Select extends FacadeModuleP {
@JSImport("react-select", JSImport.Default)
@js.native object raw extends js.Object
class Props[A] extends PropTypes with HasOpaqueReaderWriter[A] {
val options = of[Seq[A]]
val onChange = of[A => Callback]
}
override def mkProps[A] = new Props[A]
}HasOpaqueReaderWriter[A] provides Writer/Reader instances that pass values through as-is — use it when the JavaScript side treats values as opaque (e.g., option objects in a select).
The Material-UI facades are generated by extracting components and props from react-docgen.