# Navigation

## Overview

Navigation in *Shopfoo* is built on top of [Feliz.Router](https://github.com/Zaid-Ajaj/Feliz.Router), a type-safe client-side routing library for Fable/React applications. The routing logic lives in two main files:

* **`Routing.fs`** — Defines the `Page` discriminated union, URL encoding/decoding, and navigation helpers.
* **`View.fs`** — The application entry point, where the root `AppView` React component uses its own Elmish loop to manage global concerns including URL changes via `React.router`.

## The `Page` type

All navigable pages are represented by a single discriminated union:

```fsharp
type Page =
    | About
    | Admin
    | Home
    | Login
    | NotFound of url: string
    | ProductIndex of filters: Filters
    | ProductDetail of SKU
```

Each case carries its own parameters. Notably, `ProductIndex` embeds a full `Filters` record (search term, sorting, category filters...) and `ProductDetail` carries a strongly-typed `SKU`.

## Entry point: `React.router`

In `View.fs`, the top-level `AppView` component wires everything together using `React.router`:

```fsharp
React.router [
    router.pathMode
    router.onUrlChanged (Page.parseFromUrlSegments >> UrlChanged >> dispatch)
    router.children [ navbar; content; toast ]
]
```

Three things happen here:

1. **Path mode** — The router uses the URL path (not hash-based routing).
2. **URL change listener** — When the browser URL changes, `Page.parseFromUrlSegments` converts the raw segments into a typed `Page` value, which is then dispatched as an `UrlChanged` message into the Elmish loop.
3. **Children rendering** — The navbar, page content, and toast notifications are rendered as children of the router.

The model stores the current `Page`, and the view pattern-matches on it to render the appropriate page component:

```fsharp
let pageView =
    match pageToDisplayInline with
    | Page.About -> Pages.About.AboutView env
    | Page.Admin -> Pages.Admin.AdminView env
    | Page.Login -> Pages.Login.LoginView env
    | Page.ProductDetail sku -> Pages.Product.Details.Page.ProductDetailsView env sku
    | Page.ProductIndex filters -> Pages.Product.Index.Page.ProductIndexView env filters
    | ...
```

## Navigating between pages

Two helpers are defined in `Routing.fs` to trigger navigation, depending on the context:

### From a view: `Router.navigatePage`

When navigation is triggered by a user interaction (e.g. a button click), the view calls:

```fsharp
Router.navigatePage Page.About
```

This helper converts the `Page` into URL segments through the `(|PageUrl|)` active pattern (details below), then delegates to `Feliz.Router`'s `Router.navigatePath`:

```fsharp
module Router =
    let navigatePage (PageUrl pageUrl) =
        Router.navigatePath (pageUrl.Segments, queryString = pageUrl.Query)
```

### From an `update` function: `Cmd.navigatePage`

When navigation must happen as an Elmish side effect (e.g., redirecting after login), the `update` function returns:

```fsharp
Cmd.navigatePage Page.ProductIndexDefaults
```

This is the `Cmd` counterpart, producing an Elmish command that triggers the same `navigatePath` call:

```fsharp
module Cmd =
    let navigatePage (PageUrl pageUrl) =
        Cmd.navigatePath (pageUrl.Segments, queryString = pageUrl.Query)
```

Both helpers share the same `(|PageUrl|)` conversion, ensuring a single source of truth for URL generation.

### In anchor elements: `prop.hrefRouted`

For `<a>` links that need both a visible `href` (for accessibility and right-click "copy link") and SPA navigation (preventing full page reload), a helper extension is defined:

```fsharp
type prop with
    static member inline hrefRouted (PageUrl pageUrl) = [
        prop.href (Router.formatPath (pageUrl.Segments, queryString = pageUrl.Query))
        prop.onClick Router.goToUrl
    ]
```

It sets the `href` attribute for the browser and intercepts the click event via `Router.goToUrl` to perform client-side navigation instead. This is used extensively in the navbar breadcrumbs and filter tabs.

```fsharp
Html.a [
    prop.key $"nav-link-%s{page.Key}"
    prop.text text
    yield! prop.hrefRouted page
]
```

## Navbar: breadcrumb-based navigation

The `AppNavBar` component renders a DaisyUI breadcrumb trail that adapts to the current page. A private `Nav` class encapsulates the breadcrumb item rendering logic:

* If the item corresponds to the current page, it renders as plain text (no link).
* Otherwise, it renders as an anchor using `prop.hrefRouted`.

```fsharp
type private Nav(currentPage, ...) =
    // ...
    member private nav.page(page: Page, text: string, ?cssClass: string) =
        if String.IsNullOrWhiteSpace(text) then
            Html.none
        else
            Html.li [
                prop.key $"nav-%s{page.Key}"
                prop.className [
                    match cssClass with
                    | Some css -> css
                    | None -> ()
                ]
                prop.children [
                    if page = currentPage then
                        Html.text text
                    else
                        Html.a [
                            prop.key $"nav-link-%s{page.Key}"
                            prop.text text
                            yield! prop.hrefRouted page
                        ]
                ]
            ]

    // ...
```

The breadcrumb structure reflects the page hierarchy. For example, a `ProductDetail` page for a book shows: **Home > Products > Books > OL12345M**.

```fsharp
type private Nav(..., translations: AppTranslations) =
    let translate =
        if translations.IsEmpty then
            fun _ -> String.Empty
        else
            fun f -> f translations

    // ...

    member nav.Home = nav.page (Page.Home, "🛍️ Shopfoo")
    member nav.About = nav.page (Page.About, translate _.Home.About)
    member nav.Admin = nav.page (Page.Admin, translate _.Home.Admin)
    member nav.Login = nav.page (Page.Login, translate _.Home.Login)
    member nav.Products = nav.page (Page.ProductIndexDefaults, translate _.Home.Products)
    member nav.Bazaar = nav.page (Page.ProductIndexDefaultsWith _.ToBazaar(), translate _.Home.Bazaar)
    member nav.Books = nav.page (Page.ProductIndexDefaultsWith _.ToBooks(), translate _.Home.Books)
    member nav.Product sku = nav.page (Page.ProductDetail sku, text = sku.Value, cssClass = "font-semibold")

[<ReactComponent>]
let AppNavBar key currentPage pageDisplayedInline translations children =
    let nav = Nav(currentPage, translations)

    Daisy.navbar [
        Daisy.breadcrumbs [
            Html.ul [
                nav.Home
                match pageDisplayedInline with
                | Page.Home
                | Page.NotFound _ -> ()
                | Page.About -> nav.About
                | Page.Admin -> nav.Admin
                | Page.Login -> nav.Login
                | Page.ProductIndex { CategoryFilters = None } -> nav.Products

                | Page.ProductIndex { CategoryFilters = Some(CategoryFilters.Bazaar _) } ->
                    nav.Products
                    nav.Bazaar

                | Page.ProductIndex { CategoryFilters = Some(CategoryFilters.Books _) } ->
                    nav.Products
                    nav.Books

                | Page.ProductDetail sku ->
                    nav.Products

                    match sku.Type with
                    | SKUType.FSID _ -> nav.Bazaar
                    | SKUType.ISBN _ -> nav.Books
                    | SKUType.OLID _ -> nav.Books
                    | SKUType.Unknown -> ()

                    nav.Product sku
            ]
        ]

        yield! children
    ]
```

Additional controls (user dropdown, language selector, theme switcher, admin gear icon, about icon) are injected as `children` of the navbar from the `AppView`.

```fsharp
AppNavBar "app-nav" model.Page pageToDisplayInline translations [
    match fullContext.User with
    | User.Anonymous -> ()
    | User.LoggedIn(userName, _) -> UserDropdown "nav-user" userName translations logout

    LangDropdown "nav-lang" fullContext.Lang model.LangMenus startChangeLang
    ThemeDropdown "nav-theme" model.Theme translations onThemeChanged

    if not translations.IsEmpty then
        if fullContext.User.CanAccess Feat.Admin then
            Daisy.button.button [
                // ...
                prop.title translations.Home.Admin
                prop.onClick (fun _ -> Router.navigatePage Page.Admin)
            ]

        Daisy.button.button [
            // ...
            prop.title translations.Home.About
            prop.onClick (fun _ -> Router.navigatePage Page.About)
        ]
]
```

## URL encoding and decoding

### Encoding: `(|PageUrl|)` active pattern

The `(|PageUrl|)` active pattern converts a `Page` value into a `PageUrl` record containing URL segments and query parameters:

```fsharp
let (|PageUrl|) = // Page -> PageUrl
    function
    | Page.About -> PageUrl.WithSegments("about")
    | Page.ProductIndex({ CategoryFilters = Some(CategoryFilters.Books(authorId, tag)) } as filters) ->
        PageUrl
            .WithSegments("books")
            .WithQueryParamOptional("author", authorId |> Option.map (...))
            .WithQueryParamOptional("tag", tag)
            .WithFiltersQueryParams(filters)
    | ...
```

The `PageUrl` type offers a fluent API (`WithQueryParam`, `WithQueryParamOptional`, `WithFiltersQueryParams`) to build the query string incrementally. Only non-default values are serialized — for instance, `Highlighting.Active` (the default) produces no query parameter, while `Highlighting.None` serializes as `highlight=no`.

### Decoding: `Page.parseFromUrlSegments`

URL segments are parsed back into a `Page` using pattern matching:

```fsharp
let parseFromUrlSegments segments =
    match segments with
    | [] -> Page.Home
    | [ "about" ] -> Page.About
    | [ "bazaar"; Route.SKU sku ] -> Page.ProductDetail sku
    | [ "books"; Route.Query(Route.Filters changeFilters & Route.Author authorId & Route.Tag tag) ] ->
        Page.ProductIndexDefaultsWith (fun filters ->
            { changeFilters filters with CategoryFilters = Some(CategoryFilters.Books(authorId, tag)) })
    | _ -> Page.NotFound (Router.formatPath segments)
```

This function leverages a layered system of active patterns (detailed in the next section).

### Roundtrip property

A property-based test verifies that encoding and decoding are inverse operations:

```fsharp
member _.``roundtrip page`` (sanitizedPage: SanitizedPage) =
    let inputPage = sanitizedPage.Value
    let (PageUrl pageUrl) = inputPage
    let parsedPage = Page.parseFromUrlSegments pageUrl.SegmentsWithQueryString
    parsedPage =! inputPage
```

The test does not exercise arbitrary `Page` values — it uses `SanitizedPage`, a record type that constrains all string fields (SKU values, tags, author IDs, search terms...) to an `AlphaNumString` type. Its FsCheck generator is designed to produce 1 to 32 alphanumeric characters (`a-z`, `A-Z`, `0-9`), avoiding special characters that would break URL encoding or query string parsing. Since real-world pages are a subset of these sanitized pages (actual SKUs, tags, and search terms only contain alphanumeric characters), the test remains valid and guarantees that any realistic `Page` value survives a roundtrip through URL serialization and parsing.

## Deep dive: active pattern composition for URL parsing

The URL parsing in `Routing.fs` is a good example of how F# active patterns can be composed to build a clean, declarative parser. Several layers of active patterns work together, each handling a specific concern.

### Primitive helpers

At the base level, small utility patterns extract raw values:

| Pattern           | Purpose                                              |
| ----------------- | ---------------------------------------------------- |
| `(⏐Dashed⏐)`      | Splits a string on `-` into segments (total pattern) |
| `(⏐Param⏐_⏐) key` | Extracts the value of a query parameter by key       |
| `(⏐YesNo⏐_⏐)`     | Recognizes `"yes"` / `"no"` as `bool`                |
| `(⏐Col⏐_⏐)`       | Recognizes a column key like `"name"` as a `Column`  |
| `(⏐Desc⏐_⏐)`      | Recognizes the `"desc"` token                        |

### Domain-level patterns

These compose the primitives into domain-meaningful extractions:

| Pattern          | Input         | Output                                                                                 |
| ---------------- | ------------- | -------------------------------------------------------------------------------------- |
| `(⏐SKU⏐_⏐)`      | Route segment | `SKU option` — recognizes `"FS-42"`, `"BN-978..."`, `"OL123M"`                         |
| `(⏐Highlight⏐)`  | Query params  | `Highlighting` — reads `highlight` param                                               |
| `(⏐MatchCase⏐)`  | Query params  | `CaseMatching` — reads `matchCase` param                                               |
| `(⏐SearchTerm⏐)` | Query params  | `string option` — reads `search` param                                                 |
| `(⏐Sort⏐)`       | Query params  | `(Column * SortDirection) option` — parses `"name-desc"` via `Dashed` + `Col` + `Desc` |
| `(⏐Category⏐)`   | Query params  | `BazaarCategory option`                                                                |
| `(⏐Author⏐)`     | Query params  | `OLID option`                                                                          |
| `(⏐Tag⏐)`        | Query params  | `string option`                                                                        |

### Composite pattern: `(|Filters|)`

The `(|Filters|)` pattern combines four sub-patterns at once using the `&` (AND) combinator:

```fsharp
let (|Filters|) queryParams : Filters -> Filters =
    match queryParams with
    | Highlight highlighting & MatchCase caseMatching & SearchTerm searchTerm & Sort sortBy ->
        fun filters -> {
            filters with
                SortBy = sortBy
                Search = {
                    filters.Search with
                        CaseMatching = caseMatching
                        Highlighting = highlighting
                        Term = searchTerm
                }
        }
```

It returns a *function* (`Filters -> Filters`) rather than a `Filters` value. This allows callers to apply it on top of default filters, which is how `parseFromUrlSegments` works:

```fsharp
| [ "books"; Route.Query(Route.Filters changeFilters & Route.Author authorId & Route.Tag tag) ] ->
    Page.ProductIndexDefaultsWith (fun filters ->
        { changeFilters filters with CategoryFilters = Some(CategoryFilters.Books(authorId, tag)) })
```

Here, `Route.Query` extracts the query string, then `Route.Filters`, `Route.Author`, and `Route.Tag` are all applied simultaneously via `&`, each extracting its own concern from the same query parameters.

### What makes this approach elegant

This layered active pattern design achieves several things:

1. **Separation of concerns** — Each pattern handles exactly one aspect (one query param, one URL segment format). They are small, testable, and reusable.
2. **Declarative composition** — The `&` combinator lets you combine multiple extractions in a single `match` arm without nested conditionals or manual parameter lookups.
3. **Defaults via totality** — Most domain-level patterns are *total* (not partial): they always return a value, falling back to a sensible default (e.g. `CaseInsensitive`, `Highlighting.Active`). This means unrecognized or missing query params silently resolve to defaults, avoiding errors.
4. **Functional transformation** — `(|Filters|)` returning a function rather than a flat value enables a clean pipeline where category-specific parameters can be layered on top separately.
5. **Symmetry with encoding** — The key constants (e.g. `"highlight"`, `"matchCase"`) appear in both the `(|Param|_|)` calls (decoding) and the `WithQueryParamOptional` calls (encoding), making the roundtrip relationship obvious and easy to maintain.

The overall result is that `parseFromUrlSegments` reads almost like a route table declaration — each `match` arm maps a URL pattern to a `Page` case, with the active patterns handling all the parsing details behind the scenes.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://rdeneau.gitbook.io/safe-clean-architecture/front-end/navigation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
