-
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.
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 prefix fragment, you can use the
EndPoint
attribute:type Page = | [<EndPoint "/">] Home // -> / | [<EndPoint "/article">] BlogArticle of id: int // -> /article/42 | [<EndPoint "/list">] 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">] List of tags: list<string> // -> /list/2/bolero/blazor | [<EndPoint "/login">] LoginAndRedirectTo of Page // -> /login/list/2/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
}