# HTMX, Auto Refresh, and Modals

Iwf's default HTML stack is hypermedia-first. Internal links and forms are
boosted by htmx, swaps use morphdom, and the server still owns rendering.

## 1. Default Boosted Shell

The default shell includes:

- htmx
- morphdom-swap extension
- Basecoat CSS/JS
- Iwf client script
- stable flash host
- stable modal host
- canonical `#main-content`

The shell adds:

```html
hx-boost="true"
hx-target="#main-content"
hx-swap="morphdom"
hx-ext="morphdom-swap"
hx-sync="this:queue"
```

Use `hx-boost="false"` only for external links, downloads, full-page escapes,
or explicit hard-navigation cases.

## 2. Rendering Rules

`render` returns:

- full document for normal HTML requests
- only `#main-content` fragment for HTMX HTML requests
- no HTMX fragment when the request prefers JSON

This lets the same controller action serve first page loads and boosted
navigation.

For direct `Html` pages, Iwf normalizes the returned title/head and main
content internally. The HTMX response contains the extracted `#main-content`
plus a `<template data-iwf-head>` carrier with the next page head. The browser
client merges that carrier into `document.head` during the morphdom swap:

- `<title>` replaces the current title
- keyed `<meta>`, `<link>`, and `<script>` entries are upserted
- stable framework runtime assets stay in place unless the incoming document
  explicitly replaces the same keyed entry
- use `data-iwf-key` for app-owned head entries, such as a canonical link whose
  `href` changes from page to page

Use a top-level `<head>` when the page needs metadata beyond a title:

```idris
articlePage : Article -> Html
articlePage article =
  fragment
    [ <head>
      <title>{article.title}</title>
      <meta name="description" content={article.summary}>
      <link
        rel="canonical"
        href={pathTo articleRoute (MkArticleParams article.slug)}
        data-iwf-key="canonical"
      >
    </head>
    , <main id="main-content">
        <h1>{article.title}</h1>
      </main>
    ]
```

## 3. Redirects

Normal boosted forms should return:

```idris
seeOther "/articles"
```

htmx follows the redirect and swaps the inherited target.

Use:

```idris
htmxHardRedirect "/"
```

only when the whole shell must reload, for example logout or stale app shell
recovery.

## 4. Modals

The default shell owns a stable modal host outside `#main-content`. Load modal
content with:

- `modalGetAttrs`
- `modalPostAttrs`

Close and clear the host with:

- `modalCloseAttrs`
- `modalCloseButtonAttrs`
- `htmxCloseModal`

The canonical lifecycle events are:

- `htmxCloseModalEvent` (`iwf:close-modal`)
- `iwfModalOpenedEvent` (`iwf:modal-opened`)
- `iwfModalClosedEvent` (`iwf:modal-closed`)

The browser client also exposes `window.Iwf.openModal()` and
`window.Iwf.closeModal()` for code that needs to coordinate with the same modal
lifecycle.

The modal host is stable across normal page swaps, so boosted navigation does
not destroy it unless a response explicitly closes it.

## 5. Auto Refresh

Mark a live region in normal page HTML with `liveFragment`:

```idris
articleFeed : ControllerContext -> List ArticleSummary -> Html
articleFeed context articles =
  liveFragment context "article-feed" "/articles/partial" ["articles"]
    (articleList articles)
```

The fragment refresh endpoint returns the same live region:

```idris
articlesPartial : ControllerContext -> List ArticleSummary -> Response
articlesPartial context articles =
  renderFragment context
    (liveFragment context "article-feed" "/articles/partial" ["articles"]
      (articleList articles))
```

Enable the runtime and its shell markup at the app boundary:

```idris
liveLayout : PageLayout
liveLayout context page =
  { pageShellAfter := page.pageShellAfter ++ autoRefreshShell defaultAutoRefreshConfig } page

main : IO ()
main =
  runApp (app "Live app" routes |> withAutoRefresh |> withPageLayout liveLayout)
```

The page itself stays focused on main content:

```idris
livePage : ControllerContext -> List ArticleSummary -> Html
livePage context articles =
  fragment
    [ <title>Live page</title>
    , <main id="main-content">
        <h1>Live page</h1>
        {liveFragment context "article-feed" "/articles/partial" ["articles"]
          (articleList articles)}
      </main>
    ]
```

The full browser response includes metadata and client support outside
`#main-content`; HTMX receives only main content plus the head-update carrier.

The current production contract is polling plus scoped sync:

- the version endpoint reports the app/runtime version and registry revision
- the client polls the version endpoint
- when the version changes, the client posts active and removed signed session
  claims to the sync endpoint
- the sync endpoint re-renders stale active fragments and returns replacement
  HTML

The WebSocket endpoint sends reload notifications when connected. The browser
client must continue to work through polling and sync when no socket message
arrives.

Auto Refresh session ids are DOM/session registry identifiers. Iwf signs each
registered session from the current controller session and app secret; clients
send `id:token` claims, and the runtime ignores bare ids or claims with the
wrong token. Fragment refresh paths should be idempotent GET endpoints that can
reconstruct the needed request, session, and controller state from normal
application context. Iwf preserves the signed session cookie and original
request headers from the registering controller context when it re-dispatches a
stale fragment.

## 6. Sessions And Table Reads

Live regions are registered against table reads and refreshed through
idempotent GET endpoints. The app-facing API should feel like a page feature:
mark the DOM region, name the refresh path, and list the tables that make the
region stale. The lower-level signed session registry and response-header
protocol are framework integration details and live behind advanced imports.

Iwf snapshots sessions inside the swap target during HTMX swaps. Sessions in
stable shell regions stay alive when only `#main-content` is swapped.
Explicitly removed sessions are discarded immediately, and sessions that stop
appearing in sync claims are pruned after repeated inactive syncs. This keeps
stale fragments from staying registered forever after socket close, tab
navigation, or disconnected DOM regions.

TypedSQL can infer table reads when reliable. Use explicit tracking for custom
SQL:

```idris
transactionExecTracked "select * from articles" ["articles"] transaction
```

Mark writes with the tables they invalidate:

```idris
withTransactionWithAutoRefresh runtime connection $
  transactionExecInvalidating
    "update articles set title = 'Updated' where id = 1;"
    ["articles"]
```

For same-page mutations on Auto Refresh pages, prefer:

```idris
respondNoContent
```

The live refresh path updates the page without a redundant boosted redirect.

## Next

Read [Assets and Styling](assets-and-styling.md).
