This package contains a SPA (Single Page Application) router, based on Servant combinators.
The routing schema is a surjection between URLs and routes, in that more than one URL could result in the same route.
data Route = Home | About Search
We may have a URL mapping as follows:
Evolve RoutesYou can take advantage of this property to support legacy URLs and evolve a routing schema over time.
This injective mapping in the opposite direction we will call
toRoute : URL → Route. To programatically navigate the application, we need a mapping from routes to URLs. This mapping documents the canonical URL for a given route.
Let’s call this mapping
fromRoute : Route → URL
These functions abstractly would have the following relationship:
∀route. toRoute (fromRoute route) = route ∃!url. fromRoute (toRoute url) = url
Dependant TypesThese functions are not directly representable in Haskell so long as we intend on using
Using the above example, below you describe your types. Note again that URL descriptors are types, yet routes are terms.
type Search = Text data Route = Home | About Search type SPA = "v2" :> "about" :> QueryParam "search" Search :> Raw :<|> "about" :> QueryParam "search" Search :> Raw :<|> "home" :> Raw :<|> Raw
Order and SpecificityYou must consider the order of the URLs separated by
To map URLs to routes, use a servant combinator exposed by this package
routes :: SPA :>> Route routes = About (1) :<|> About :<|> Home :<|> Home
|1||Because you used
Order is importantThe burden is on the developer to put the right constructor at each location in the Servant specification, which while typically safe, is not fool-proof.
The mapping of routes to URLs is a bit inelegant at the moment. It should be possible to eliminate in future releases with the use of
instance Routed SPA Route where redirect = \case Home -> Redirect (Proxy @("home" :> Raw)) id About search -> Redirect (Proxy @("v2" :> "about" :> QueryParam "search" Search :> Raw)) ($ search)
The above mapping is type safe, and URLs not present in the
SPA type will not compile. Any terms that can be captured in the pattern matching must be used for this instance to be lawful.
To use the router, it is expected that you use a function to start your application (such as fullPageSPA), where there is a
(r → m a) argument and where you can describe how to obtain an application state based on a given route. This is useful both for pure frontend applications, as well as applications that use isomorphic rendering with the static renderer.
This is typically a relationship between Routes and a sum type of page data. You can use the
(r → m a) to perform IO and populate our model with data:
data Model = HomePage | AboutPage AboutPage data AboutPage = AboutPage Search [BlogPost] initial :: MonadJSM m => Route -> m Model initial = \case Home -> return HomePage About search -> do posts <- getBlogPosts AboutPage search $ searchFilter search posts
For an example that takes advantage of server-side rendering, isomorphic
Servant route sharing, see the Servant CRUD Example.