-
-
Save Mahoney/3ed811f6d5e5f5f32292cfc3d8004029 to your computer and use it in GitHub Desktop.
Stateful SPA
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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