Iwf Guide

Iwf Guide

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:

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:

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:

seeOther "/articles"

htmx follows the redirect and swaps the inherited target.

Use:

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:

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:

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:

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:

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:

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

Mark writes with the tables they invalidate:

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

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

respondNoContent

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

Next

Read Assets and Styling.