Navigation with Fable.Elmish
This post is my contribution to the 5th #FsAdvent calendar. Thanks to everyone involved, you make the F# world better.
Elmish provides several helpers for building large-scale single-page applications, including not only Model-Update lifecycle, but also navigation APIs for browsers. In this article I would like to go through the possible types of Elmish navigation.
Nowadays modern browsers support two types of routing:
- Hash-based (when SPA handles the changes only in the hash part of the URL)
- Push-state (when SPA handles the changes in the whole path of the URL thanks to window.history API)
Fable.Elmish supports both of them.
This type of routing is supported even with the oldest browsers, since only the hash part of the URL is being changed; and initially hash was used for anchor links on the same page. It can implemented easily with
UrlParser.parseHash helper. The simplest example we could even imagine:
We have a tiny application, with a model, containing only the current page. Depending on the selected page we show either “Home page” or “About page” text. To update the model with the current page, we rely on
parseHash helper, but to handle the mapping, we need to provide a parser (
pageParser). In our case it has only basic string mappings to
about terms with a fallback to
home for root(
/) path. When a parser finds the appropriate route, it passes the page to our
urlUpdate function, where we finally update the model if the URL matches some route, otherwise we modify the URL to fallback to Home page. At this point we can get the arguments (path, query string). We also need to implement
init function with a parser
result argument, so we could update the model with the current page at the initialization stage.
Note that this application is totally simplified, it doesn’t even have messages, so the
update function doesn’t do anything, just returns the same model.
Hash-based routing meets all requirements of any possible SPA. With it you just use links with hashes, and Elmish subscribes to hash changes for the navigation. This approach is handy, but I would say, hash is a bit outdated, URLs without it look much better.
Push-state routing is visually much nicer. URLs look like regular links with a path, but there is no full reload of a page. However, in Elmish it requires manual control on the links. Elmish.Navigation has an event
let [<Literal>] internal NavigatedEvent = "NavigatedEvent". Unfortunately, it is internal, but we can use the string to cheat a bit. ;)
So let’s implement a wrapper element, which we can use to trigger this event.
First of all we create
LinkProp type with necessary properties:
To— for replacement of
Replace— if we want to replace the current history state instead of pushing a new one (
Secondly, we implement the same not as a discriminated union, but as an interface, to be able to use the props as an strongly typed object.
Then, we implement the link itself. It is just a function with two arguments:
children. It wraps the passed children with an
a element, but with handling its
OnClick event. In the handler, we check:
if the user passed custom
OnClick handler, just invoke it;
if not, we check that
defaultPrevented = false, the click was not modified by any button, such as
shift or meta key, that the link doesn’t have
Target "_blank" and the link is clicked only with the left mouse button.
If all conditions are met, we can be sure that it is a standard link click. So we prevent default, depending on
Replace property we push or replace the history state and then we trigger the
"NavigatedEvent", which will be handled by Elmish.
We also want to pass the rest of properties to the
a element, so we filter out
HTMLAttr.Href props and concat the list with our
For push-state routing, Elmish app implementation will be pretty much the same, except that you need to use
parsePath instead of
parseHash and our custom
link instead of just
Comparing the above mentioned types of routing, I, personally, prefer the latter, since it is more modern, using the latest history API and with better URL look. There are a couple of drawbacks, of course: browser compatibility and manual handling of the links, which is a bit cheaty in the provided implementation. However, I believe, that Elmish is stable enough and we can make the
P.S. The implementation of
link element is inspired by react-router project, which has almost the same approach.