Included is a router (in the orbit of Single-Page Applications) that is written entirely in Scala.
The package is japgolly.scalajs.react.extra.router
.
libraryDependencies += "com.github.japgolly.scalajs-react" %%% "extra" % "2.1.1"
- Features
- Caution
- Creating & Using a Router
- Creating a
RouterConfig
RouterCtl
- Beyond the Basics
- Examples
- Type-safety.
- Use your own types to uniquely identify routes and their parameters.
- Link URLs are guaranteed to be valid.
- Routes for different pages or routing rule sets cannot be used in the wrong context.
- Rules
- Routes to views.
- Redirection routes.
- Dynamic routes. (eg.
/person/123
) - Default values in dynamic routes.
- URL re-writing / translation rules. (eg. can remove trailing slashes from URL.)
- Choose to redirect or render custom view when route is invalid / not found.
- Routes can be nested and modularised.
- Conditions can be applied to an entire route set.
- Router can indicate the current page with precision, faciliating dynamic menus and breadcrumbs even in the presence of complex, dynamic routes.
- Route views can be wrapped in a layout. (eg. to add page headers, footers, a nav breadcrumb.)
- URL and view are always kept in sync.
- Routes are bookmarkable.
- Uses HTML5 History API.
- Callback for route changes.
- Routing logic is easily unit-testable.
- Optional pass-through properties (allowing, for example, passing global state to child components).
-
If you want routes starting with slashes, you will need to configure your server appropriately. There's no point having
www.blah.com/foo
have routes like/bar
if when the server receives a request forwww.blah.com/foo/bar
it doesn't know to use the same endpoint aswww.blah.com/foo
. If you don't have that control, begin with a#
instead, like#foo
. -
It's a security feature of browsers that if a user enters a different URL, you can't absorb it with your SPA router. Using
window.onbeforeunload
you can only prompt the user to change their mind and keep the current URL. If a user manually enters a URL to move from one part of your SPA to a different part of the SPA, it's going to reload the page... unless you use#
in your URL and they change the portion after the#
. In such a case you can use thewindow.onhashchange
event handler. -
If you use Internet Explorer v0.1 ~ v9, the HTML5 API won't be available. But that's ok, there's no need to code like our homo-heidelbergensis ancestors, just download and use a polyfill.
-
These is a small but significant guarantee that this design sacrifices to buy important features. See A spot of unsafeness.
You need two things to create a router.
- A
RouterConfig
, describing your routes and other rules. (Detail below.) - A
BaseUrl
, describing the URL prefix that is the same for all your routes.
The BaseUrl
needn't be absolute at compile-time, but it needs to be absolute at runtime. BaseUrl.fromWindowOrigin
will give you the protocol, domain and port at runtime, after which you should append a path if necessary. Example: BaseUrl.fromWindowOrigin / "my_page"
instead of using BaseUrl("http://blah.com/my_page")
or BaseUrl("http://127.0.0.1:8080/my_page")
directly.
Once you have these, pass them to Router(baseUrl, config)
to receive a ReactComponent
for your router.
Rendering the router is the same as any other React component; just create an instance and call render()
.
@JSExport
override def main(): Unit = {
val router = Router(baseUrl, routerConfig)
router().renderIntoDOM(dom.document.body)
}
First, you'll want to create a data representation of your pages. For example:
sealed trait MyPages
case object Home extends MyPages
case object Search extends MyPages
case object Products extends MyPages
case class ProductInfo(id: ProductId) extends MyPages
Next, you'll want to call RouterConfigDsl.buildConfig
and use the provided DSL.
import japgolly.scalajs.react.extra.router._
// import japgolly.scalajs.react.vdom.Implicits._
// ↑ Depending on your usage you might also need this
// ↑ If you have VDOM-related compilation errors, add this import.
val routerConfig = RouterConfigDsl[MyPages].buildConfig { dsl =>
import dsl._
// TODO Add routing rules here
// ( <rule1> | <rule2> | ... | <ruleN> )
// .notFound( <action> )
}
Details of rule & config constituents follow.
Each route can be associated with an action. The following actions are available:
DSL | Args | Description |
---|---|---|
render |
VdomElement |
Render something. |
renderR |
RouterCtl => VdomElement |
Render something using a RouterCtl . |
dynRender |
Page => VdomElement |
Render something using the current page. * Dynamic routes only. |
dynRenderR |
(Page, RouterCtl) => VdomElement |
Render something using the current page, and a RouterCtl .* Dynamic routes only. |
redirectToPage |
(Page) (implicit Redirect.Method) |
Redirect to a page. |
redirectToPath |
(Path | String) (implicit Redirect.Method) |
Redirect to a path (a URL suffix proceding the BaseUrl ). |
In the redirect actions, unless you declare your own redirect method, you'll need to specify one manually. (Eg. redirectToPage(Home)(Redirect.Push)
).
Two redirect methods are available:
Redirect.Push
- The current URL will be recorded in history. User can hit Back button to reach it.Redirect.Replace
- The current URL will not be recorded in history. User can't hit Back button to reach it.
A Route[X]
is required as input to the higher-level rule-building functions (staticRoute()
, staticRedirect()
etc).
It represents a path that requires an X
to generate, and provides an X
when parsed.
To construct a Route
, the DSL provides a route-builder RouteB
which composes nicely
and is automatically converted to a finalised Route
when used.
-
RouteB[Unit]
- Implicit conversion from
String
, like"user/edit"
. - Implicit conversion from
Path
, likeroot
which is an alias forPath.root
.
- Implicit conversion from
-
RouteB[Int]
- Use DSLint
. -
RouteB[Long]
- Use DSLlong
. -
RouteB[String]
from a URL substring - Use DSLstring(regex)
, likestring("[a-z0-9]{1,20}")
- Best to use a whitelist of characters, eg.
[a-zA-Z0-9]+
. - Do not capture groups; use
[a-z]+
instead of([a-z]+)
. - If you need to group, use non-capturing groups like
(?:bye|hello)
instead of(bye|hello)
.
- Best to use a whitelist of characters, eg.
-
RouteB[String]
from the remainder of the unmatched URL.remainingPath
- Captures the (non-empty) remaining portion of the URL path.remainingPathOrBlank
- Captures the (potentially-empty) remaining portion of the URL path.
-
RouteB[UUID]
- Use DSLuuid
. -
RouteB[Seq[(String, String)]]
- Use DSLqueryToSeq
, useful in capturing query parameters of the standard url form (http://www.myexample.com/index.html?param1=abc¶m2=123) -
RouteB[Map[String, Seq[String]]
- Use DSLqueryToMultimap
, useful in capturing query parameters of the standard url form (http://www.myexample.com/index.html?param1=abc¶m2=123) -
RouteB[Map[String, String]
- Use DSLqueryToMap
, useful in capturing query parameters of the standard url form (http://www.myexample.com/index.html?param1=abc¶m2=123) -
Composition
a ~ b
concatenatesa
tob
.
Example:"abc" ~ "def"
is the same as"abcdef"
.a / b
addsa
tob
with a literal/
in between.
Example:"abc" / "def"
is the same as"abc/def"
.- The types of each route (except
Unit
) are added together into a tuple.
Example:"grp" / int / "item" / int
is aRouteB[(Int, Int)]
.
Example:"grp" / int / "item" / int ~ "." ~ long
is aRouteB[(Int, Int, Long)]
.
-
Combinators on any
RouteB[A]
.filter(A => Boolean)
causes the route to ignore parsed values which don't satisfy the given filter..option
makes this subject portion of the route optional and turns aRouteB[A]
into aRouteB[Option[A]]
. Forms an isomorphism betweenNone
and an empty path..pmap[B](A => Option[B])(B => A)
allows you to attempt to map the route type from anA
to aB
, or fail. (prism map).xmap[B](A => B)(B => A)
allows you to map the route type from anA
to aB
. (exponential map).caseClass[A]
maps the route type(s) to a case class..caseClassDebug[A]
as above, but shows you the code that the macro generates.- If you're using the
monocle
module andimport MonocleReact._
you also gain access to:.pmapL[B](Prism[A, B])
..xmapL[B](Iso[A, B])
.
-
Combinators on
RouteB[Option[A]]
.withDefault(A)
- Specify a default value. Returns aRouteB[A]
. Uses==
to compareA
s to the given default..parseDefault(A)
- Specify a default value when parsing. (Path generation ignores this default.) Returns aRouteB[A]
.
-
Combinators on
RouteB[Unit]
.const(A)
changes aRouteB[Unit]
into aRouteB[A]
by assigning a constant value (not used in route parsing or generation).
Examples:
// Static routes
val r: Route[Unit] = root
val r: Route[Unit] = "user/profile"
// "user/3/profile" <=> 3
val r: Route[Int] = "user" / int / "profile"
// "category/bikes/item/17" <=> ("bikes", 17)
val r: Route[(String, Int)] = "category" / string("[a-z0-9]{1,20}") / "item" / int
// "cat/3/item/17" <=> Product(3, 17)
case class Product(category: Int, item: Int)
val r: Route[Product] = ("cat" / int / "item" / int).caseClass[Product]
// "get" <=> "json"
// "get.zip" <=> "zip"
val r: Route[String] = "get" ~ ("." ~ string("[a-z]+")).option.withDefault("json")
// "category/widgets/item/12345678-1234-1234-1234-123456789012" <=> Item("widgets", 12345678-1234-1234-1234-123456789012
case class Item(category: String, itemId: java.util.UUID)
val r: Route[Item] = ("category" / string("[a-z]+") / item / uuid).caseClass[Item]
Syntax:
staticRoute(Route[Unit], Page) ~> <action>
staticRedirect(Route[Unit]) ~> <redirect>
Route[Unit]
, <action>
, and <redirect>
are all described above.
Examples:
staticRoute(root, Home) ~> render( <.h1("Welcome!") )
staticRoute("#hello", Hello) ~> render(HelloComponent())
staticRedirect("#hey") ~> redirectToPage(Hello)(Redirect.Replace)
Syntax:
<dynamic route> ~> <action>
<dynamic route> ~> (P => <action>)
DSL | Args | Description |
---|---|---|
dynamicRoute |
[P <: Page](Route[P])(PartialFunction[Page, P]) |
A dynamic route using a page subtype: P .A partial function must be provided to extract a possible P from any given Page . |
dynamicRouteF |
[P <: Page](Route[P])(Page => Option[P]) |
A dynamic route using a page subtype: P .A total function must be provided to extract an Option[P] from any Page . |
dynamicRouteCT |
[P <: Page](Route[P]) |
A dynamic route using a page subtype: P .The CT suffix here denotes that this method uses a compiler-provded ClassTag to identify the page subtype P . This is equivalent to {case p: P => p} .Note that if this is used, the entire space of P is associated with a route - do not add another route over P . |
dynamicRedirect |
[A](Route[A]) |
A dynamic path not associated with a page. Any A extracted by the route may then be used to determine the redirect target. |
Example: This creates a route in the format of item/:id
.
case class ItemPage(id: Int) extends MyPage
val itemPage = ScalaComponent.builder[ItemPage]("Item page")
.render(p => <.div(s"Info for item #${p.id}"))
.build
dynamicRouteCT("item" / int.caseClass[ItemPage])
~> dynRender(itemPage(_))
To create a RouterConfig
the syntax is (<rule1> | ... | <ruleN>).notFound(<action>)
.
Rule composition is achieved via |
.
If two rules overlap (eg. can respond to the same URL), the left-hand side of |
has precedence and wins.
The .notFound()
method declares how unmatched routes will be handled.
It takes an Action
as described above, the same kind that is used in the rules.
You will generally redirect to the root, or render a 404-like page.
Once .notFound()
is called you will have a RouterConfig
.
Rules can no longer be added, but different (optional) configuration becomes available.
Example:
val routerConfig = RouterConfigDsl[Page].buildConfig { dsl =>
import dsl._
(emptyRule
| staticRoute(root, Home) ~> render(HomePage.component())
| staticRoute("#hello", Hello) ~> render(<.div("TODO"))
| staticRedirect("#hey") ~> redirectToPage(Hello)(Redirect.Replace)
) .notFound(redirectToPage(Home)(Redirect.Replace))
}
(The use of emptyRule
is just for nice formatting.)
If you'd like to see what's happening, you can call .logToConsole
on your config and to have the routing engine log what it does to the JS console. Add it anytime between .notFound()
and its use in creating a Router
.
A tradeoff in safety has been made to purchase many new features in the redesigned v2 Router. Namely, it's possible to accidentally forget a route for a page type.
Consider this example:
sealed trait MyPages
case object Page1 extends MyPages
case class Page2(value: Option[Int]) extends MyPages
case object Page3 extends MyPages
val config = RouterConfigDsl[MyPages].buildConfig { dsl =>
import dsl._
( emptyRule
| staticRoute("page1", Page1) ~> render(???)
| dynamicRouteCT("page2" ~ ("/" ~ int).option.caseClass[Page2]) ~> render(???)
// oops! We forgot Page3!!
).notFound(???)
}
To mitigate this, RouterConfig
comes with a verify(page1, ... pageN)
method that will confirm that each specified page:
- Has an associated route.
- Is itself returned when parsing its associated route.
- Has an associated action.
If any errors are detected they are displayed both on screen and in the console.
There is also a detectErrors
method which is more appropriate for unit-testing.
Amending the above example, we get:
val config = RouterConfigDsl[MyPages].buildConfig { dsl =>
import dsl._
( emptyRule
| staticRoute("page1", Page1) ~> render(???)
| dynamicRouteCT("page2" ~ ("/" ~ int).option.caseClass[Page2]) ~> render(???)
// oops! We still forgot Page3 but it will be detected at run- or test- time
).notFound(???)
.verify(Page1, Page2(None), Page2(Some(123)), Page3) // ← Ensure pages are configured and valid
}
You can also call .fallback
on your rules immediately before calling .notFound()
.
This will give you control over what happens when a page isn't configured (if you want to do something other than crashing.)
RouterCtl is a client API to the router. It allows you to control the current page, create links, determine page URLs, etc.
To use it, pass it in to components that need it via their props.
As RouterCtl[P]
has a page-type context (the P
!),
if a component only wants/needs to control a router with a certain subset of pages,
the component can accept a RouterCtl[PageSubset]
instead of a RouterCtl[AllPages]
.
A conversion to the former is just a .contramap
or .narrow
call away for the parent.
Here a subset of useful methods. Use IDE auto-complete or check the source for the full list.
-
link(Page): VdomTag
- Create a link to a page.ctl.link(Specials)("Today's Specials", ^.color := "red")
-
set___
- Programmatically navigate to route when invoked.set(Page): Callback
- Return a procedure that will navigate to route.setEH(Page): ReactEvent => Callback
- Consume an event and set the route.setOnClick(Page): TagMod
- Set the route when the subject is clicked.
Shorthand for^.onClick ==> ctl.setEH(page)
.
-
refresh: Callback
- Refresh the current route when invoked.^.button("Refresh", ^.onClick --> ctl.refresh)
-
urlFor(Page): AbsUrl
- Get the absolute URL of a given page.
The following can create rules that simply rewrite (i.e. live redirect) URLs.
They can be composed with other rules via |
as usual.
Method | Args | Description |
---|---|---|
rewritePath |
PartialFunction[Path, Redirect] |
Run a Path through a partial function that results in a redirect. |
rewritePathF |
Path => Option[Redirect] |
Run a Path through a total function that optionally results in a redirect. |
rewritePathR |
Pattern, Matcher => Option[Redirect] |
Match a Path against regex and if it matches, use the match result to optionally redirect. |
Example: This would remove leading dots.
rewritePathR("^\\.+(.*)$".r, m => Some(redirectToPath(m group 1)(Redirect.Replace)))
A few rules are included out-of-the-box for you to use:
removeTrailingSlashes
- uses a replace-state redirect to remove trailing slashes from route URLs.removeLeadingSlashes
- uses a replace-state redirect to remove leading slashes from route URLs.trimSlashes
- uses a replace-state redirect to remove leading and trailing slashes from route URLs.
There are cases in which you may want to create a route that
- Loosely matches a URL so that it can handle variations.
- Has a single appropriate URL that you want to use after variations have been accepted and parsed.
You may be creating an issue tracker that has URLs for each ticket like:
/issue/DEV-4
/issue/DEV-42
/issue/FRONTEND-23
You also want to accept imperfections such as:
/issue/DEV-004
/issue/DEV42
/issue/frontend-23
When an imperfect URL is parsed you want to auto-correct it like:
/issue/DEV-004 → /issue/DEV-4
/issue/DEV42 → /issue/DEV-42
/issue/frontend-23 → /issue/FRONTEND-23
When a URL is already perfect, you render a page normally.
There are two features you need to implement this functionality.
First, create a route as you normally would, then map its type using a prism.
To do so, and then call .pmap
(or .pmapL
to use use a Monocle prism).
Second, one you have created you route rule, call .autoCorrect
.
By default it will do a replace-state to change the URL meaning that only the correct URL will appear in the user's history -
pressing back will go back to the page before they entered the imperfect URL.
You can use push-state by using .autoCorrect(Redirect.Method)
but be warned, unless you're doing something magic/crazy,
when the user hits their back button they will request the imperfect URL again which will just redirect them forward negating their back action.
This is the implementation for the example scenario described above.
sealed trait Page
case object Home extends Page
case class IssuePage(projectCode: String, number: Int) extends Page {
def toUrlFrag: String = projectCode + "-" + number
}
val cfg = RouterConfigDsl[Page].buildConfig { dsl =>
import dsl._
def homeRoute =
staticRoute(root, Home) ~> render(<.h1("Home"))
val urlRegex = """([a-zA-Z]+)-?(\d+)""".r
def parse(urlFrag: String): Option[IssuePage] =
urlFrag match {
case urlRegex(code, num) => Some(IssuePage(code.toUpperCase, num.toInt))
case _ => None
}
def issueRoute =
dynamicRouteCT("issue" / remainingPath.pmap(parse)(_.toUrlFrag)) ~>
dynRender(renderIssuePage) autoCorrect
def renderIssuePage(p: IssuePage) =
<.div("Issue = " + p)
( homeRoute
| issueRoute
).notFound(redirectToPage(Home)(Redirect.Replace))
}
When you have a Rule
, you can call addCondition
on it to evaluate a condition every time it is used.
When the condition is met, the route is usable; when unmet, a fallback behaviour can be actioned or the router can continue processing other rules as if the conditional one didn't exist.
When using conditional routes, it's important to understand some details and take care:
-
conditional routes are selected over unconditional routes. e.g. you can have two routes at path
/
, one for logged in users (i.e. conditional) and one for public access (i.e. unconditional); in which case the conditional route has first priority with the unconditonal route being the fallback. -
conditional routes must be unambiguous and not conflict. eg. if you have route A with condition A, and route B with condition B, both at the same path; the conditions must be disjoint. If conditions A and B are ever true at the same time, it will result in a runtime exception (yuk).
-
because of the chance of runtime exceptions, it's more important to test your routing config. There is a
.verify(pages*)
method onRouterCfg
s that you can use. I suggest writing a unit test in which you modify state to create all possible route conditions, and when in each condition call.verify
with the relevant pages. It's a good idea to do this even with unconditional routes, it's just even more important with conditions.def addCondition (condition: CallbackTo[Boolean]) def addConditionWithFallback (condition: CallbackTo[Boolean], fallback: Action[Page]) def addConditionWithOptionalFallback(condition: CallbackTo[Boolean], fallback: Option[Action[Page]] def addConditionWithOptionalFallback(condition: CallbackTo[Boolean], fallback: Page => Option[Action[Page]] def addCondition (condition: Page => CallbackTo[Boolean]) def addConditionWithFallback (condition: Page => CallbackTo[Boolean], fallback: Action[Page]) def addConditionWithOptionalFallback(condition: Page => CallbackTo[Boolean], fallback: Option[Action[Page]] def addConditionWithOptionalFallback(condition: Page => CallbackTo[Boolean], fallback: Page => Option[Action[Page]]
Example:
def grantPrivateAccess: CallbackB =
???
val privatePages = (emptyRule
| staticRoute("private-1", PrivatePage1) ~> render(???)
| staticRoute("private-2", PrivatePage2) ~> render(???)
)
.addCondition(grantPrivateAccess)(_ => redirectToPage(AccessDenied)(Redirect.Push))
Once you have a RouterConfig
, you can call .renderWith
on it to supply your own render function that will be invoked each time a route is rendered. It takes a function in the shape: (RouterCtl[Page], Resolution[Page]) => VdomElement
where a Resolution
provides:
Member | Type | Description |
---|---|---|
page |
Page |
Data representation (or command) of what will be drawn. |
render |
() => VdomElement |
The render function provided by the rules and logic in RouterConfig |
Thus using the given RouterCtl
and Resolution
you can wrap the page in a layout, link to other pages, highlight the current page, etc.
See Examples for a live demonstration.
You'll likely want to update your page's title to reflect the current route being shown. To do so, call one of:
.setTitle(Page => String)
.setTitleOption(Page => Option[String])
Example:
val routerConfig = RouterConfigDsl[Page].buildConfig { dsl =>
import dsl._
( staticRoute(root, Home) ~> render(???)
| staticRoute("#about", About) ~> render(???)
)
.notFound(???)
.setTitle(p => s"PAGE = $p | Example App") // ← available after .notFound()
}
Each time a route is rendered, the "post-render" callback is invoked. Out-of-the-box, the default action is to scroll the window to the top.
You can add your own actions by calling .onPostRender
on your RouterConfig
instance.
You can set the entire callback (i.e. override instead of add) using .setPostRender
.
Both .onPostRender
and .setPostRender
take a single arg: (Option[Page], Page) => Callback
.
The function is provided the previously-rendered page (or None
when a router renders its first page),
and the current page.
Example:
val routerConfig = RouterConfigDsl[Page].buildConfig { dsl =>
import dsl._
( staticRoute(root, Home) ~> render(???)
| staticRoute("#about", About) ~> render(???)
)
.notFound(???)
.onPostRender((prev, cur) => // ← available after .notFound()
Callback.log(s"Page changing from $prev to $cur.")) // ← our callback
}
Hey? Why wrap in Callback
?
It gives you a guarantee by me and the Scala compiler that whatever you put inside, will only be executed in a callback. Underlying code won't accidently call it now when installing the callback, and then do nothing when the callback actually executes. It's not an esoteric concern - those kind of mistakes do happen in real-world code.
Routes can be created and used as modules. This is how:
- Use
RouterConfigDsl[InnerPage].buildRule
to create a (composite) rule instead of a fullRouterConfig
. The content will be your normal routing rules up until, and excluding, thenotFound()
call. - Have components that need a
RouterCtl
use aRouterCtl[InnerPage]
as normal - this will continue to work correctly regardless of whetherInnerPage
is nested or not.
All of this happens in the method in which you build your RouterConfig[OuterPage]
.
- Call one of the
pmap
methods (prism-map) on theInnerPage
rule in order to bridge the inner & outer page types. - Use
prefixPath
orprefixPath_/
on the nested routes to mount them with a prefix. (Actually you can usemodPath
if a prefix isn't enough.) - Append the result to your routes as per usual.
Example:
sealed trait Module
object Module {
case object ModuleRoot extends Module
case object ModuleDetail extends Module
val routes = RouterConfigDsl[Module].buildRule { dsl =>
import dsl._
(emptyRule
| staticRoute(root, ModuleRoot) ~> render(???)
| staticRoute("detail", ModuleDetail) ~> render(???)
)
}
}
sealed trait Outer
object Outer {
case object Root extends Outer
case object Login extends Outer
case class Nest(m: Module) extends Outer
val config = RouterConfigDsl[Outer].buildConfig { dsl =>
import dsl._
(emptyRule
| staticRoute(root, Root) ~> render(???)
| staticRoute("#login", Login) ~> render(???)
| Module.routes.prefixPath_/("#module").pmap[Outer](Nest){ case Nest(m) => m } // Much nest. Wow.
).notFound(???)
}
}
If we imagine BaseUrl
to be http://www.example.com/
then the sitemap for Outer
in this example is:
URL | Page |
---|---|
http://www.example.com/ |
Root |
http://www.example.com/#login |
Login |
http://www.example.com/#module |
Nest(ModuleRoot) |
http://www.example.com/#module/detail |
Nest(ModuleDetail) |
Sometimes you don't want your Router
to be the top-level component. In these cases, it's possible to configure the Router
component to receive Props
, which will be optionally forwarded to children components, the layout function, post-render functions, etc. A typical usage case for this setup is if you have a wrapping component that manages the application state.
To achieve this:
- Write the router configuration within a
RouterWithPropsConfigDsl[Page, Props].buildConfig
(instead of aRouterConfigDsl[Page].buildConfig
). - Create the router by invoking
RouterWithProps(baseUrl, routerConfig)
(instead ofRouter(baseUrl, routerConfig)
). The resulting component will acceptProps
as component properties and pass them through wherever your configuration instructs it to.
In the configuration DSL you can use the following in addition to the regular actions:
DSL | Args |
---|---|
renderP |
Props => VdomElement |
renderRP |
(RouterCtl, Props) => VdomElement |
dynRenderP |
(Page, Props) => VdomElement |
dynRenderRP |
(Page, RouterCtl, Props) => VdomElement |
Additionally, the following configurations functions are available in addition to the regular ones:
Function | Args |
---|---|
renderWithP |
(RouterCtl[Page], ResolutionWithProps[Page, Props]) => Props => VdomElement) |
setPostRenderP |
(Option[Page], Page, Props) => Callback) |
onPostRenderP |
(Option[Page], Page, Props) => Callback) |
setTitleP |
(Page, Props) => String) |
setTitleOptionP |
(Page, Props) => Option[String]) |
As can be seen, renderWithP
now provides a ResolutionWithProps[Page, Props]
(instead of a Resolution[Page]
), which in turn provides a renderP
function which takes Props
as parameters.
Example:
case class AppState(loggedUser: String, ...)
val routerConfig = RouterWithPropsConfigDsl[Page, AppState].buildConfig { dsl =>
import dsl._
(emptyRule
| staticRoute(root, Home) ~> renderP(appState => HomePage.component(appState))
| staticRoute("#hello", Hello) ~> renderP(appState => <.div(s"Hello again ${appState.loggedUser}!"))
| staticRedirect("#hey") ~> redirectToPage(Hello)(Redirect.Replace)
) .notFound(redirectToPage(Home)(Redirect.Replace))
.renderWithP( (ctl, r) => appState =>
<.div(s"Welcome ${appState.loggedUser}")(
r.renderP(appState)
)
)
}
val RouterComponent = RouterWithProps(baseUrl, routerConfig)
RouterComponent(appState)
The github pages for this project online at https://japgolly.github.io/scalajs-react/ uses this router and demonstrates a number of features.
- The source begins here: GhPages.scala
- Router logging is enabled so you can read what the router does in the console.
There are also unit tests available in the japgolly.scalajs.react.extra.router package.
This simple example demonstrates routing as well.