-
Notifications
You must be signed in to change notification settings - Fork 54
Routing
Bolero provides facilities to bind the page's URL to the Elmish model.
In order to use routing, you need to make sure that the page's base URL is set correctly. In most cases, you simply need to add the following inside the <head>
of your page:
<base href="/">
If the root of your application is a subpath, then you need to set that subpath as the base, including the trailing slash. For example, if you have an application served at https://someone.github.io/myrepo/
, then you need the following:
<base href="/myrepo/">
The easiest way to create a router is by using an inferred router. In this mode of operation, you create an endpoint type which has a 1-to-1 correspondance with your supported URLs, and store it in the Elmish model.
Here are the steps to set up an inferred router:
-
Create the endpoint type. Typically, it will be an F# union type where each case corresponds to a path prefixed by the case's name and the case's arguments are the consecutive fragments of the path. For example:
type Page = | Home // -> /Home | BlogArticle of id: int // -> /BlogEntry/42 | BlogList of user: string * page: int // -> /BlogList/tarmil/1
See Format for all the supported types and how to customize the corresponding path.
-
Add a field in the Elmish model that stores the endpoint:
type Model = { page: Page // other fields... }
-
Add an Elmish message that sets the endpoint:
type Message = | SetPage of Page // other messages... let update message model = match message with | SetPage page -> { model with page = page } // other messages...
-
Create the router with
Router.infer
:let router = Router.infer SetPage (fun m -> m.page)
-
Attach the router to the Elmish program:
Program.mkSimple initModel update view |> Program.withRouter router
The router has a few helpful utilities:
-
router.Link
takes an endpoint value and returns the corresponding URL.a [attr.href (router.Link Home)] [text "Go to Home"]
-
router.HRef
takes an endpoint and returns anhref
attribute pointing to the corresponding URL.a [router.HRef Home] [text "Go to Home"]
Router.infer
supports the following types:
-
Standard library types:
string
bool
- integer:
int8
(akasbyte
),uint8
(akabyte
),int16
,uint16
,int32
(akaint
),uint32
,int64
,uint64
decimal
- float:
single
,double
(akafloat
)
-
F# union types. Each case corresponds to a path prefixed by the case's name. The case's arguments are the consecutive fragments of the path:
type Page = | Home // -> /Home | BlogArticle of id: int // -> /BlogEntry/42 | BlogList of user: string * page: int // -> /BlogList/tarmil/1
To customize the path, you can use the
EndPoint
attribute, with parameters indicated by{curly_braces}
.type Page = | [<EndPoint "/">] Home // -> / | [<EndPoint "/article/{id}">] BlogArticle of id: int // -> /article/42 | [<EndPoint "/list/{user}/{page}">] BlogList of user: string * page: int // -> /list/tarmil/1
An
{*asterisk}
indicates that this parameter catches the rest of the path. It must be the last parameter in the path, and correspond to a value of typestring
,list
orarray
.type Page = | [<EndPoint "/list/{user}/tagged/{*tags}">] ListTagged of user: string * tags: list<string> Tagged("tarmil", ["bolero"; "webassembly"]) // -> /list/tarmil/tagged/bolero/webassembly
Several cases can share a common prefix in their paths, as long as any parameters in this common prefix have the same type.
type Page = | [<EndPoint "/user/{user}/favorites">] Favorites of user: string | [<EndPoint "/user/{user}/comments">] Comments of user: string
The path must contain a
{parameter}
for each of its case's values. Alternatively, the path can consist of a single non-parameter fragment; in this case, the values are appended in order as separate fragments.type Page = | [<EndPoint "/list">] // Equivalent to /list/{user}/{page} BlogList of user: string * page: int // -> /list/tarmil/1
-
Tuples. The values of the tuple are the consecutive fragments of the path.
type Page = int * string // -> /42/abc
-
F# record types. The fields of the record are the consecutive fragments of the path.
type Page = { x: int y: string } // -> /42/abc
-
Lists and arrays. The first fragment of the path is the number of items, and the items themselves are the consecutive fragments.
type Page = list<string> // -> /3/abc/def/ghi
-
Any combination of the above, including recursive types.
type Page = | [<EndPoint "/article">] Article of ArticleId // -> /article/123/announcing-bolero | [<EndPoint "/list/{*tags}">] List of tags: list<string> // -> /list/bolero/blazor | [<EndPoint "/login/{page}">] LoginAndRedirectTo of page: Page // -> /login/list/bolero/blazor and ArticleId = { uid: int slug: string }
To have more control over the exact shape of your URLs, you can create a custom router like follows.
let customRouter : Router<Page, Model, Message> =
{
// getEndPoint : Model -> Page
getEndPoint = fun m -> m.page
// setRoute : string -> option<Message>
setRoute = fun path ->
match path.Trim('/').Split('/') with
| [||] -> Some Home
| [|"article"; id|] -> Some (BlogArticle (int id))
| [|"list"; user; page|] -> Some (BlogList (user, int page))
| _ -> None
|> Option.map SetPage
// getRoute : Page -> string
getRoute = function
| Home -> "/"
| BlogArticle(id) -> sprintf "/article/%i" id
| BlogList(user, page) -> sprintf "/list/%s/%i" user page
}
Note: if the URL is changed in such a way that setRoute
returns None
, then the model is not updated.
You can also create a router that maps directly to the model without an intermediary endpoint type. However this router type doesn't have some utilities such as Link
and HRef
.
let customRouter2 : Router<Model, Message> =
{
// setRoute : string -> option<Message>
setRoute = fun path ->
match path.Trim('/').Split('/') with
| [||] -> Some Home
| [|"article"; id|] -> Some (BlogArticle (int id))
| [|"list"; user; page|] -> Some (BlogList (user, int page))
| _ -> None
|> Option.map SetPage
// getRoute : Model -> string
getRoute = fun model ->
match model.page with
| Home -> "/"
| BlogArticle(id) -> sprintf "/article/%i" id
| BlogList(user, page) -> sprintf "/list/%s/%i" user page
}