A simple router for single page applications, written in Elm. Inspired by angular-ui-router and Rails router
Elm nested router allows to separate application logic to handlers binded to specific routes. For every Route
you can provide a list of actions that has to be performed on entering to Route
, and a view
function that renders Route
specific HTML parts.
To create a routeable application we have to keep Router
state (Current route and params) in application state.
In order to do this we create our application state by using a helper WithRouter
:
import Router
import Router.Types exposing (WithRouter)
-- We use `WithRouter` to define application state
type alias State = WithRouter Route
{
categories: List Category,
posts: List Post,
post: Maybe Post
}
-- construct initialState with initial values
initialState : State
initialState = {
router = Router.initialState
, categories = []
, posts = []
, post = Nothing
}
Next step will be the definition of application routes:
type Route = Home | NotFound | Static String | Category | Post
-- Route list that is required by router.
-- Note that routes which registered first have higher priority to be matched. So when you have concurrent routes, order of this list is important.
routes : List Route
routes = [
NotFound
, Static "about"
, Static "contacts"
, Home
, Category
, Post
]
Route Post
on the example above is rely on routes Category
and Home
- that means all actions binded to routes Home
, Category
and Post
will be executed to enter to Post
route. Full URL template that will match Post
route will also be combined with its parent routes.
Category
route is concurrent to Static "about"
("\about" - can match both Category
and Static
routes) so Static "about"
will be matched there since it's declared first. Otherwise Category
with a param "about" will be matched
Another thing that we need to define is a mapping between routes and route configurations:
routeConfig : Route -> RouteConfig Route State
routeConfig route = case route of
Home -> {
segment = "/",
parent = Nothing,
bypass = False,
constraints = Dict.empty,
handler = homeHandler
}
NotFound -> {
segment = "/404",
parent = Nothing,
bypass = False,
constraints = Dict.empty,
handler = notFoundHandler
}
Static page -> {
segment = "/" ++ page,
parent = Nothing,
bypass = False,
constraints = Dict.empty,
handler = staticHandler page
}
Category -> {
-- `:category` and `:subcategory` is dynamic route params
-- `:category` param match only "animals", "flowers", "colors" because of its constraints
-- `:subcategory` might be omitted, since it enclosed brackets
segment = ":category[/:subcategory]",
parent = Just Home,
bypass = False,
constraints = Dict.fromList [("category", Enum ["animals", "flowers", "colors"])],
handler = categoryHandler
}
Post -> {
-- `:postId` must be an integer
segment = "/post/:postId",
parent = Just Category,
bypass = False,
constraints = Dict.fromList [("postId", Int)],
handler = postHandler
}
Each handler provides named views: Dict String Html
- these HTML parts are finally combined and rendered in application layout:
layout : Router Route State -> State -> Dict String (Html (Action State)) -> Html (Action State)
layout router _ views =
let
defaultHeader = Html.header [] [Html.text "Default header"]
defaultFooter = Html.footer [] [Html.text "Default footer"]
defaultBody = Html.div [] []
in Html.div [] [
Maybe.withDefault defaultHeader <| Dict.get "header" views
, Maybe.withDefault defaultBody <| Dict.get "body" views
, Maybe.withDefault defaultFooter <| Dict.get "footer" views
]
Finally we launch Router:
main : Program Never
main = Router.dispatch
(always <| noFx initialState)
(RouterConfig {
html5 = False
, removeTrailingSlash = True
, transition = \router _ to -> case to of
Nothing -> router.redirect (Home, Dict.empty)
Just route -> let
_ = (Debug.log "onTransition" route)
in doNothing
, layout = layout
, routes = routes
, routeConfig = config
, subscriptions = always Sub.none
})
see Example (Live demo) and Tests for more details.
- HTML5 push state
- Route params
- Params constraints (String, Int, Enum, Regexp)
- Optional params
- Named views
- Drop dependencies on libs
- Better handling for trailing slashes
- Greedy matcher config (matches first found route or latest one / bypass route config)
- Check the possibility to replace router cache with Automaton
- Improve perfomance on specific functions
- Query string parameters support