elmish-browser


Parsing routes

The library defines building blocks for processing URLs in a way that assigns types to the values extracted. The idea is to define a parser in type-safe fashion, using combinators:

  • s combinator for a static string we expect to find in the URL,
  • </> combinator to capture a slash in the URL,
  • str combinator to extract a string,
  • i32 combinator to attempt to parse an int.
  • top combinator that takes nothing.

Some examples:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
#r "Fable.Elmish.Browser.dll"

open Elmish.Browser.UrlParser

parse (s "blog" </> i32) "blog/42" // Some 42

parse (s "blog" </> i32) "blog/13" //  Some 13

parse (s "blog" </> i32) "blog/hello" // None

parse (s "search" </> str) "search/dogs" // Some "dogs"

parse (s "search" </> str) "search/13" // Some "13"

parse (s "search" </> str) "search" // None

Normally you want to put many of these parsers together to handle all possible routes. The following parser works on URLs like /blog/42 and /search/badger:

1: 
2: 
3: 
4: 
5: 
6: 
type Route = Blog of int | Search of string

let route =
    oneOf
        [ map Blog (s "blog" </> i32)
          map Search (s "search" </> str) ]

Here we are turning URLs into nice union types, so we can use case expressions to work with them in a nice way. oneOf will try the listed parsers one by one until it finds one that returns Some. The map function in this example passes the outputs from the parser into the the case constructors.

1: 
2: 
3: 
4: 
5: 
parse route "blog/58"    // Some (Blog 58)
parse route "search/cat" // Some (Search "cat")
parse route "search/31"  // Some (Search "31")
parse route "blog/cat"   // None
parse route "blog"       // None

Note that F# case constructors take all of the arguments as a tuple, while the parser will apply the arguments individually, so we may need to adapt the signature:

1: 
2: 
3: 
4: 
5: 
6: 
type Route2 = BlogDouble of int * string // needs arguments in tupled form

let curry f x y = f (x,y) // convert tupled form function of two arguments into curried form

let route2 state =
    map (curry BlogDouble) (s "blog" </> i32 </> str) state

Now the compiler is happy for the two arguments to be passed individually to the curried case constructor.

Handling URL updates in your application

Now that we've parsed the route into a nice data structure, we need to handle the updates. For that we'll define urlUpdate function that takes the current state and the parsed output, for example:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
type Model =
  { route : Route
    query : string }

open Elmish.Browser.Navigation

let urlUpdate (result:Option<Route>) model =
  match result with
  | Some (Search query) ->
      { model with route = result.Value; query = query }, [] // Issue some search Cmd instead

  | Some page ->
      { model with route = page; query = "" }, []

  | None ->
      ( model, Navigation.modifyUrl "#" ) // no matching route - go home
    

It looks like update function but instead of the message it handles the route changes. If the URL is valid, we just update our model or issue a command, otherwise we modify the URL to whatever makes sense.

Now we augument our program instance with Navigation capabilities, passing the parser and urlUpdate:

1: 
2: 
3: 
4: 
5: 
open Elmish

Program.mkProgram init update view
|> Program.toNavigable (parseHash route) urlUpdate
|> Program.run

Working with full (HTML5) and hash-based URLs

parseHash function works with "hashbang" URLs, i.e. everything after the '#' symbol, while parsePath works with the entire location. The query for a hashbang URL never leaves the browser and can appear in traditional tags like <a>. Working with full URL on the other hand means you have to be careful about which requests you want forwarded to the server and which ones should be handled locally. To keep the request local you have to use Navigation module for all the URL transitions.

Make sure you understand which one you need.

namespace Elmish
namespace Elmish.Browser
module UrlParser

from Elmish.Browser
val parse : parser:Parser<('a -> 'a),'a> -> url:string -> args:Map<string,string> -> 'a option

Full name: Elmish.Browser.UrlParser.parse
val s : str:string -> Parser<'a,'a>

Full name: Elmish.Browser.UrlParser.s
val i32 : state:State<(int -> 'a)> -> State<'a> list

Full name: Elmish.Browser.UrlParser.i32
val str : state:State<(string -> 'a)> -> State<'a> list

Full name: Elmish.Browser.UrlParser.str
type Route =
  | Blog of int
  | Search of string

Full name: Routing.Route
union case Route.Blog: int -> Route
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
union case Route.Search: string -> Route
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
val route : (State<(Route -> Route)> -> State<Route> list)

Full name: Routing.route
val oneOf : parsers:('a -> 'b list) list -> state:'a -> 'b list

Full name: Elmish.Browser.UrlParser.oneOf
val map : subValue:'a -> parse:Parser<'a,'b> -> Parser<('b -> 'c),'c>

Full name: Elmish.Browser.UrlParser.map
type Route2 = | BlogDouble of int * string

Full name: Routing.Route2
union case Route2.BlogDouble: int * string -> Route2
val curry : f:('a * 'b -> 'c) -> x:'a -> y:'b -> 'c

Full name: Routing.curry
val f : ('a * 'b -> 'c)
val x : 'a
val y : 'b
val route2 : state:State<(Route2 -> 'a)> -> State<'a> list

Full name: Routing.route2
val state : State<(Route2 -> 'a)>
type Model =
  {route: Route;
   query: string;}

Full name: Routing.Model
Model.route: Route
Model.query: string
namespace Elmish.Browser.Navigation
val urlUpdate : result:Option<Route> -> model:Model -> Model * Elmish.Sub<'a> list

Full name: Routing.urlUpdate
val result : Option<Route>
module Option

from Microsoft.FSharp.Core
val model : Model
union case Option.Some: Value: 'T -> Option<'T>
val query : string
property Option.Value: Route
val page : Route
val query : Linq.QueryBuilder

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.query
union case Option.None: Option<'T>
module Navigation

from Elmish.Browser.Navigation
val modifyUrl : newUrl:string -> Elmish.Cmd<'a>

Full name: Elmish.Browser.Navigation.Navigation.modifyUrl
Multiple items
module Program

from Elmish

--------------------
module Program

from Elmish.Browser.Navigation

--------------------
type Program<'arg,'model,'msg,'view> =
  {init: 'arg -> 'model * Cmd<'msg>;
   update: 'msg -> 'model -> 'model * Cmd<'msg>;
   subscribe: 'model -> Cmd<'msg>;
   view: 'model -> Dispatch<'msg> -> 'view;
   setState: 'model -> Dispatch<'msg> -> unit;
   onError: string * exn -> unit;}

Full name: Elmish.Program<_,_,_,_>
val mkProgram : init:('arg -> 'model * Cmd<'msg>) -> update:('msg -> 'model -> 'model * Cmd<'msg>) -> view:('model -> Dispatch<'msg> -> 'view) -> Program<'arg,'model,'msg,'view>

Full name: Elmish.Program.mkProgram
val toNavigable : parser:Parser<'a> -> urlUpdate:('a -> 'model -> 'model * Cmd<'msg>) -> program:Program<'a,'model,'msg,'view> -> Program<unit,'model,Navigable<'msg>,'view>

Full name: Elmish.Browser.Navigation.Program.toNavigable
val parseHash : parser:Parser<('a -> 'a),'a> -> location:Fable.Import.Browser.Location -> 'a option

Full name: Elmish.Browser.UrlParser.parseHash
val urlUpdate : result:Option<Route> -> model:Model -> Model * Sub<'a> list

Full name: Routing.urlUpdate
val run : program:Program<unit,'model,'msg,'view> -> unit

Full name: Elmish.Program.run