Skip to content

Routing

Loic Denuziere edited this page Nov 28, 2018 · 9 revisions

Bolero provides facilities to bind the page's URL to the Elmish model.

Inferred router

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:

  1. 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.

  2. Add a field in the Elmish model that stores the endpoint:

    type Model =
        {
            page: Page
            // other fields...
        }
  3. 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...
  4. Create the router with Router.infer:

    let router = Router.infer SetPage (fun m -> m.page)
  5. 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 an href attribute pointing to the corresponding URL.

    a [router.HRef Home] [text "Go to Home"]

Format

Router.infer supports the following types:

  • Standard library types:

    • string
    • bool
    • integer: int8 (aka sbyte), uint8 (aka byte), int16, uint16, int32 (aka int), uint32, int64, uint64
    • decimal
    • float: single, double (aka float)
  • 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
        }

Custom router

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
    }
Clone this wiki locally