Skip to content

Instantly share code, notes, and snippets.

@Mahoney
Forked from japgolly/stateful_spa.scala
Last active September 29, 2018 22:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Mahoney/3ed811f6d5e5f5f32292cfc3d8004029 to your computer and use it in GitHub Desktop.
Save Mahoney/3ed811f6d5e5f5f32292cfc3d8004029 to your computer and use it in GitHub Desktop.
Stateful SPA
// Overview of how this works:
// 1. Initialisation data is used to create an instance of LoadedRoot.
// 2. LoadedRoot contains a component which is the virtual top-level component.
// It uses the initialisation data and can be sure that it won't change (a guarantee you don't have with component props).
// It's the only component to have state.
// The state can applies to the entire SPA, all routes.
// It gets told by the router which page to render.
// It can house logic that applies when certain pages change to certain other pages.
// 3. LoadedRoot is passed to Routes.routerConfig.
// Routes.routerConfig creates a config that sends all routes to the LoadedRoot component, using the Page & RouterCtl as props.
// 4. Main creates a router using the RouterConfig and renders it.
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
package statefulrouter
import japgolly.scalajs.react
import japgolly.scalajs.react.Callback
import japgolly.scalajs.react.component.Js
import japgolly.scalajs.react.component.Scala.BackendScope
import japgolly.scalajs.react.extra.OnUnmount
import japgolly.scalajs.react.extra.router.{BaseUrl, Router}
import japgolly.scalajs.react.vdom.html_<^.^._
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom
import statefulrouter.LoadedRoot.{Props, State}
import statefulrouter.Routes.{ChangeState, Page, Root, RouterCtl, ShowState}
object Main {
def main(): Unit = {
val root = new LoadedRoot("initial state")
val baseUrl = BaseUrl.fromWindowOrigin_/
val router = Router(baseUrl, Routes.routerConfig(root))
router().renderIntoDOM(dom.document.getElementById("container"))
}
}
object Routes {
import japgolly.scalajs.react.extra.router.{RouterCtl => RouterCtl_, _}
type RouterCtl = RouterCtl_[Page]
sealed trait Page
case object Root extends Page
case object ShowState extends Page
case object ChangeState extends Page
def routerConfig(rootInstance: LoadedRoot) =
RouterConfigDsl[Page].buildConfig { dsl =>
import dsl._
def render(page: Page, r: RouterCtl) =
rootInstance.Component(LoadedRoot.Props(page, r))
val dynPage = dynRenderR((page: Page, r) => render(page, r))
def staticPage(route: StaticDsl.Route[Unit], page: Page) =
staticRoute(route, page) ~> renderR(r => render(page, r))
(emptyRule
| staticPage(root, Root)
| staticPage("show", ShowState)
| staticPage("change", ChangeState)
)
.notFound(Root)
.renderWith { (rctl, resolution: Resolution[Page]) =>
<.div(cls:="wrapper")(
<.div(id:="page_name")(resolution.page.toString),
resolution.render(),
rctl.link(ShowState)(id:="showLink")("show link"),
rctl.link(ChangeState)(id:="changeLink")("change link"),
)
}
}
}
object LoadedRoot {
case class Props(page: Page, routerCtl: RouterCtl)
case class State(value: String)
}
final class LoadedRoot(val initialState: String) {
val Component = react.ScalaComponent.builder[Props]("LoadedRoot")
.initialState({
State(initialState)
})
.renderBackend[Backend]
.build
}
final class Backend($: BackendScope[Props, State]) extends OnUnmount {
def render(p: Props, s: State): VdomElement = {
p.page match {
case Root => <.div(id:="wrapper")(
<.div(id:="the_state")("None on the Root Page")
)
case ShowState => ShowStateComponent(s.value, p.routerCtl).vdomElement
case ChangeState => ChangeStateComponent(s.value, p.routerCtl, this).vdomElement
}
}
def changeState(newState: String): Callback = {
$.setState(State(newState))
}
}
object ShowStateComponent {
private case class Props(toShow: String, routerCtl: RouterCtl)
private val Component = react.ScalaComponent.builder[Props]("ShowState")
.render_P { p =>
<.div(id:="the_state")(
p.toShow
)
}
.build
def apply(toShow: String, routerCtl: RouterCtl): Js.UnmountedSimple[_, _] =
Component.apply(Props(toShow, routerCtl: RouterCtl))
}
object ChangeStateComponent {
private case class Props(toShow: String, routerCtl: RouterCtl, ops: Backend)
private val Component = react.ScalaComponent.builder[Props]("ChangeState")
.render_P { p =>
<.div(
id:="the_state",
onClick --> p.ops.changeState("NEW STATE!")
)(
"Opportunity to change "+p.toShow
)
}
.build
def apply(
toShow: String,
routerCtl: RouterCtl,
ops: Backend
): Js.UnmountedSimple[_, _] =
Component.apply(Props(toShow, routerCtl, ops))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment