# elm-route-craft

Composable, bidirectional SPA routing for Elm 0.19.1. Define your routes once as a typed codec pipeline and get both URL parsing and URL building from the same definition - no code generation, no CLI, no magic. It is a library you wire up yourself, not a framework that owns your app structure.

## Quick Start

```elm
module Main exposing (main)

import App
import Html exposing (a, div, h1, text)
import Html.Attributes exposing (href)
import Route exposing (Router)
import Url exposing (Url)

type Route
    = Home
    | Blog
    | BlogPost Int
    | NotFound

router : Router Route
router =
    Route.oneOf
        [ Route.top Home
        , Route.codec Blog (\r -> case r of Blog -> Just () ; _ -> Nothing)
            |> Route.literal "blog"
        , Route.codec BlogPost (\r -> case r of BlogPost id -> Just ( id, () ) ; _ -> Nothing)
            |> Route.literal "blog"
            |> Route.int
        ]

toRoute : Url -> Route
toRoute url =
    Route.fromUrl router url |> Maybe.withDefault NotFound

main =
    App.create
        { router = router
        , toRoute = toRoute
        , toPage = \route _ ->
            App.page
                { init = \_ -> ( (), Cmd.none )
                , update = \_ m -> ( m, Cmd.none )
                , view = \_ _ -> div [] [ h1 [] [ text (Debug.toString route) ] ]
                , subscriptions = \_ _ -> Sub.none
                }
        , shared =
            { init = \_ -> ( (), Cmd.none )
            , update = \_ s -> ( s, Cmd.none )
            , subscriptions = \_ -> Sub.none
            }
        }
```

`Route.parse router "/blog/42"` returns `Just (BlogPost 42)`.
`Route.toPath router (BlogPost 42)` returns `Just "/blog/42"`.

## Features

- **Bidirectional routing** - parse and build URLs from a single codec definition, no duplication
- **Typed query parameters** - `withQuery`, `parseFull`, `toFullPath`, and a record-friendly `succeed`/`with`/`build` pipeline give you typed query params alongside path params
- **Rich segment primitives** - `int`, `string`, `float`, `uuid`, `nonEmptyString`, plus `splat` for variable-length tails and `custom` for anything else
- **Constrained segments** - `intRange min max`, `slug`, and `enum` validate values at parse time and refuse to build URLs that would not round-trip
- **Case-insensitive literals** - `literalCi` parses any case but builds the canonical form, useful for SEO redirect hygiene
- **Nested routes with shared prefixes** - `nest "blog" [ ... ]` groups routes under a common prefix and can be nested arbitrarily
- **Mountable base paths** - `withBasePath "/admin"` mounts the entire router at a sub-path
- **Hash routing** - `parseHash`/`toHashPath`/`fromUrlHash` for static hosts that cannot rewrite paths
- **Active link helpers** - `startsWith` for highlighting nav links when the current route is under a section
- **Canonicalization** - `canonicalize` / `redirectTo` drive redirect-to-canonical handlers without users threading `parse |> toPath` themselves
- **Strict and Result-typed query parsing** - `Query.required` plus `parseQueryStrict` (returns `Maybe`) or `parseQueryResult` (returns `Result (List String) a` listing missing keys)
- **Raw query helpers** - `Query.update`, `drop`, `toList`, `fromList`, `lookup`, `merge`, `preserve`, `except`, `canonicalString` for query operations that don't fit the typed pipeline
- **Composable page hierarchy** - `App.page` supports static, sandbox, and element-style pages via `init`/`update`/`view`/`subscriptions`
- **Shared state** - first-class `shared` record threaded through page `init`, `view`, and `subscriptions`
- **No code generation** - define routes in plain Elm, the compiler checks your extractor tuples against your constructors
- **No CLI required** - install and start writing routes

## Comparison

| Feature | elm-route-craft | elm-land | elm-spa | Vanilla Elm |
|---|---|---|---|---|
| Code generation required | No | Yes | Yes | No |
| CLI required | No | Yes | Yes | No |
| Bidirectional routing | Yes | No | No | Manual |
| Typed query parameters | Yes | Partial | No | Manual |
| Nested route prefixes | Yes | File-based | No | Manual |
| Page types (init/update/view/subs) | Yes | Yes | Yes | Yes |
| Shared state | Yes | Yes | Partial | Manual |
| Route guards / onEnter hooks | Yes | Partial | No | Manual |
| Composable layouts | Yes | Yes | No | Manual |
| Plain Elm, no framework lock-in | Yes | No | No | Yes |

## Examples

- **[minimal](examples/minimal/src/Main.elm)** - 3-page SPA with no shared state and no layouts. The fastest way to see the full wiring.
- **[blog](examples/blog/)** - Multi-page blog with nested routes, shared state, and typed query parameters on the search page.
- **[dashboard](examples/dashboard/)** - Authenticated dashboard with layouts, route guards, and shared user state.
- **[features](examples/features/)** - Tour of the additional path primitives and helpers: `splat`, `float`, `nonEmptyString`, `uuid`, `startsWith`, the `Query.succeed`/`with`/`build` pipeline, and `Query.withDefault`.
- **[hash-routing](examples/hash-routing/)** - Same SPA shape, but routes live in the URL fragment via `parseHash` / `toHashPath` / `fromUrlHash` for static hosts that cannot rewrite paths.

## Documentation

- [docs/GUIDE.md](docs/GUIDE.md) - End-to-end walkthrough from installation to a full SPA
- [docs/API.md](docs/API.md) - Full API reference for `Route` and `App`
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) - Design decisions and internals
- [docs/MIGRATION.md](docs/MIGRATION.md) - Migration guide from elm-spa, elm-land, or hand-rolled routing

## Installation

```
elm install joshburgess/elm-route-craft
```

## License

BSD-3-Clause
