Skip to content
An Elmish router that is focused, powerful yet extremely easy to use. Made for developer happiness.
F# JavaScript Other
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.paket Initial commit 🚀 Aug 16, 2019
.vscode Initial commit 🚀 Aug 16, 2019
demo
public Initial commit 🚀 Aug 16, 2019
src Publish v1.4 🚀 thanks to @kerams, see #5 Sep 14, 2019
tests Fix decoding query string parameters twice, fixes #3 Aug 20, 2019
.gitattributes Initial commit 🚀 Aug 16, 2019
.gitignore
.travis.yml Initial commit 🚀 Aug 16, 2019
LICENSE Initial commit 🚀 Aug 16, 2019
Nuget.Config Initial commit 🚀 Aug 16, 2019
README.md Fix typo Sep 2, 2019
appveyor.yml Setup Appveyor CI with tests Aug 19, 2019
build.fsx Setup Appveyor CI with tests Aug 19, 2019
package-lock.json Initial commit 🚀 Aug 16, 2019
package.json Setup Appveyor CI with tests Aug 19, 2019
paket-install.sh Initial commit 🚀 Aug 16, 2019
paket.dependencies Initial commit 🚀 Aug 16, 2019
paket.lock Update Feliz to 0.46.0 Aug 28, 2019
publish.js Initial commit 🚀 Aug 16, 2019
webpack.config.js Initial commit 🚀 Aug 16, 2019

README.md

Feliz.Router Nuget Build status

An Elmish router that is focused, powerful yet extremely easy to use. Made for developer happiness.

Here is a full example

open Feliz
open Feliz.Router

type State = { CurrentUrl : string list }
type Msg = UrlChanged of string list

let init() = { CurrentUrl = [ ] }

let update (UrlChanged segments) state =
    { state with CurrentUrl = segments }

let render state dispatch =
    let currentPage =
        match state.CurrentUrl with
        | [ ] -> Html.h1 "Home"
        | [ "users" ] -> Html.h1 "Users page"
        | [ "users"; Route.Int userId ] -> Html.h1 (sprintf "User ID %d" userId)
        | _ -> Html.h1 "Not found"

    Router.router [
        Router.onUrlChanged (UrlChanged >> dispatch)
        Router.application currentPage
    ]

Program.mkSimple init update render
|> Program.withReactSynchronous "root"
|> Program.run

Installation

dotnet add package Feliz.Router

The package includes a single element called router that is to be included at the very top level of your render or view function

let render state dispatch =

    let currentPage = Html.h1 "App"

    Router.router [
        Router.onUrlChanged (UrlChanged >> dispatch)
        Router.application currentPage
    ]

Where it has two primary properties

  • Router.onUrlChanged : string list -> unit gets triggered when the url changes where it gives you the url segments to work with.
  • Router.application: ReactElement the element to be rendered as the single child of the router component, usually here is where your root application render function goes.

Router.onUrlChanged is everything

Routing in most applications revolves around having your application react to url changes, causing the current page to change and data to reload. Here is where Router.onUrlChanged comes into play where it triggers when the url changes giving you the cleaned url segments as a list of strings. These are sample urls their corresposing url segments that get triggered as input of of onUrlChanged:

segment "#/" => [ ]
segment "#/home" => [ "home" ]
segment "#/home/settings" => [ "home"; "settings" ]
segment "#/users/1" => [ "users"; "1" ]
segment "#/users/1/details" => [ "users"; "1"; "details" ]

// with query string parameters
segment "#/users?id=1" => [ "users"; "?id=1" ]
segment "#/home/users?id=1" => [ "home"; "users"; "?id=1" ]
segment "#/users?id=1&format=json" => [ "users"; "?id=1&format=json" ]
// escaped query string parameters are decoded when the url is segmented
segment @"#/search?q=whats%20up" => [ "search"; "?q=whats%20up" ]

Parsing URL segments into Page definitions

Instead of using overly complicated parser combinators to parse a simple structure such as URL segments, the Route module includes a handful of convenient active patterns to use against these segments:

type Page =
    | Home
    | Users
    | User of id:int
    | NotFound

type State = { CurrentPage : Page }

type Msg = PageChanged of Page

let update (PageChanged nextPage) state =
    { state with CurrentPage = nextPage }

// string list -> Page
let parseUrl = function
    // matches #/ or #
    | [ ] ->  Page.Home
    // matches #/users or #/users/ or #users
    | [ "users" ] -> Page.Users
    // matches #/users/{userId}
    | [ "users"; Route.Int userId ] -> Page.User userId
    // matches #/users?id={userId} where userId is an integer
    | [ "users"; Route.Query [ "id", Route.Int userId ] ] -> Page.User userId
    // matches everything else
    | _ -> NotFound

let render state dispatch =
    let currentPage =
        match state.CurrentPage with
        | Home -> Html.h1 "Home"
        | Users -> Html.h1 "Users page"
        | User userId -> Html.h1 (sprintf "User ID %d" userId)
        | NotFound -> Html.h1 "Not Found"

    Router.router [
        Router.onUrlChanged (parseUrl >> PageChanged >> dispatch)
        Router.application currentPage
    ]

Of course, you can define your own patterns to match against the route segments, just remember that you are working against simple string.

Programmatic Navigation

Aside from listening to manual changes made to the URL by hand, the Router.router element is able to listen to changes made programmatically from your code with Router.navigate(...). This function is implemented as a command and can be used inside your update function.

The function Router.navigate has the general syntax:

Router.navigate(segment1, segment2, ..., segmentN, [query string parameters], [historyMode])

Examples of the generated paths:

Router.navigate("users") => "#/users"
Router.navigate("users", "about") => "#/users/about"
Router.navigate("users", 1) => "#/users/1"
Router.navigate("users", 1, "details") => "#/users/1/details"

Examples of generated paths with query string parameters

Router.navigate("users", [ "id", 1 ]) => "#/user?id=1"
Router.navigate("users", [ "name", "john"; "married", "false" ]) => "#/users?name=john&married=false"
// paramters are encoded automatically
Router.navigate("search", [ "q", "whats up" ]) => @"#/search?q=whats%20up"
// Pushing a new history entry is the default bevahiour
Router.navigate("users", HistoryMode.PushState)
// to replace current history entry, use HistoryMode.ReplaceState
Router.navigate("users", HistoryMode.ReplaceState)

Here is a full example in an Elmish program.

type State = { CurrentUrl : string list }

type Msg =
    | UrlChanged of string list
    | NavigateToUsers
    | NavigateToUser of int

let init() = { CurrentUrl = [ ] }, Cmd.none

let update msg state =
    match msg with
    | UrlChanged segments -> { state with CurrentUrl = segments }, Cmd.none
    // notice here the use of the command Router.navigate
    | NavigateToUsers -> state, Router.navigate("users")
    // Router.navigate with query string parameters
    | NavigateToUser userId -> state, Router.navigate("users", [ "id", userId ])

let render state dispatch =

    let currentPage =
        match state.CurrentUrl with
        | [ ] ->
            Html.button [
                prop.text "Navigate to users"
                prop.onClick (fun _ -> dispatch NavigateToUsers)
            ]
        | [ "users" ] ->
            Html.div [
                prop.children [
                    Html.h1 "Users page"
                    Html.button [
                        prop.text "Navigate to User(10)"
                        prop.onClick (fun _ -> dispatch (NavigateToUser 10))
                    ]
                ]
            ]

        | [ "users"; Route.Query [ "id", Route.Int userId ] ] ->
            Html.h1 (sprintf "Showing user %d" userId)

        | _ -> Html.h1 "Not found"

    Router.router [
        Router.onUrlChanged (UrlChanged >> dispatch)
        Router.application currentPage
    ]
You can’t perform that action at this time.