# Add interactive behavior

Use htmx to update part of a page with server-rendered HTML. This guide builds a
search form that works with htmx and with ordinary navigation.

Cicadel bundles htmx 4. Your Go application serves the script, so you need no CDN.

## Ask your agent

```text
Read https://cicadel.fauzaanu.com/en/dev/howto/htmx/index.md.
Add a GET search form to my application using Cicadel's bundled htmx.
Register the script route, include the script once, and render a full page for
ordinary requests and a results fragment for partial requests.
Keep authorization in the handler and test both responses, including escaped input.
```

## 1. Register the script and search routes

Add `apps/search/app.go`:

<!-- example: htmx/apps/search/app.go -->
```go
package search

import (
    "log/slog"
    "net/http"
    "strings"

    "github.com/fauzaanu/cicadel"
    "github.com/fauzaanu/cicadel/htmx"
    "github.com/fauzaanu/cicadel/response"
    "github.com/fauzaanu/cicadel/ui"
    "github.com/fauzaanu/cicadel/urls"
)

func New() cicadel.App {
    return cicadel.App{
        Name: "search",
        Routes: []urls.Route{
            urls.Path("GET", htmx.ScriptPath, "search:script", htmx.Handler().ServeHTTP),
            urls.Path("GET", "/search", "search:index", Search),
        },
    }
}

func Search(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query().Get("q")
    items := []string{}
    for _, title := range []string{"Go notes", "PostgreSQL notes", "UI notes"} {
        if strings.Contains(strings.ToLower(title), strings.ToLower(query)) {
            items = append(items, title)
        }
    }
    err := htmx.HTML(w, r, http.StatusOK,
        ui.Fragment(page(query, items)),
        ui.Fragment(results(items)),
    )
    if err != nil {
        slog.ErrorContext(r.Context(), "render search", "error", err)
        if !ui.Committed(err) {
            response.Error(w, http.StatusInternalServerError)
        }
    }
}
```

- `htmx.Handler()` serves the bundled script at `htmx.ScriptPath`.
- `htmx.HTML` takes two renderings: the full `page` and the `results` fragment.
  It sends the fragment for htmx partial requests and the full page otherwise.

For database-backed search, replace the sample list with a query scoped to the
current user's permitted rows. Use the same permissions for full and partial requests.

## 2. Include the script and render results

Add `apps/search/page.templ`:

<!-- example: htmx/apps/search/page.templ -->
```templ
package search

import "github.com/fauzaanu/cicadel/htmx"
import "github.com/fauzaanu/cicadel/ui"
import "github.com/fauzaanu/cicadel/ui/layout"

templ page(query string, items []string) {
    @layout.Page.Call(layout.Props{Title: "Search"}, layout.Slots{
        Scripts: htmx.Script(),
        Children: ui.Fragment(searchForm(query, items)),
    })
}

templ searchForm(query string, items []string) {
    <h1>Search notes</h1>
    <form action="/search" method="get" hx-get="/search" hx-target="#results">
        <label for="query">Search</label>
        <input id="query" name="q" type="search" value={ query }/>
        <button type="submit">Search</button>
    </form>
    <section id="results" aria-live="polite">
        @results(items)
    </section>
}

templ results(items []string) {
    if len(items) == 0 {
        <p>No results.</p>
    }
    <ul>
        for _, item := range items {
            <li>{ item }</li>
        }
    </ul>
}
```

- `Scripts: htmx.Script()` includes htmx once, in the full document.
- The partial response contains only the results markup.
- Text inside `{ ... }` is escaped.

## 3. Register the app

In `project/apps.go`, import `example.com/myapp/apps/search` and add `search.New()`
to the existing list:

```go
func InstalledApps(_ *gorm.DB) ([]cicadel.App, error) {
    return []cicadel.App{
        notes.New(),
        search.New(),
    }, nil
}
```

Keep your other app registrations. Use your own module path in the import.

## 4. Build and run

```sh
go tool cicadel ui build
go tool cicadel ui check --json
go test -race ./...
go run manage.go runserver
```

Open `http://127.0.0.1:8000/search`. You can also check both response forms:

```sh
curl 'http://127.0.0.1:8000/search?q=Go'
curl -H 'HX-Request: true' -H 'HX-Request-Type: partial' \
  'http://127.0.0.1:8000/search?q=Go'
```

| Request | Response |
| --- | --- |
| Ordinary | A full HTML document. |
| htmx partial | The `<ul>` results, with no document wrapper. |

`htmx.HTML` also sets the required `Vary` headers. It returns a full page for
history restoration.

## POST and DELETE requests

The example uses a read-only GET. For writes, keep your app's authentication and
CSRF middleware, and send its CSRF token.

### CSRF for htmx requests

Pass the session's token to the page layout. Every htmx request from the page then
sends it as the `X-CSRF-Token` header, which `security.Sessions.CSRF` accepts:

```templ
@layout.Page.Call(layout.Props{Title: "Notes", CSRFToken: token}, layout.Slots{
    Scripts:  htmx.Script(),
    Children: ui.Fragment(notes(items)),
})
```

- `token` comes from your session, for example `browser.CSRFToken(r.Context())`.
- The layout renders `<body hx-headers:inherited='{"X-CSRF-Token":"…"}'>`.
  `htmx.CSRFHeaders(token)` returns the same value for your own layouts.
- Pages that set the token must not be stored by shared caches. The CSRF
  middleware already sends `Cache-Control: no-store`.

Ordinary forms that must also work without JavaScript still need the hidden field:

```templ
<form action="/notes/delete" method="post">
    @form.CSRF.Call(form.CSRFProps{Token: token}, ui.NoSlots{})
    <button hx-delete="/notes/42" hx-target="#notes">Delete note</button>
</form>
```

`form` is `github.com/fauzaanu/cicadel/ui/form`.

> [!WARNING]
> htmx headers are not authorization. Check object permissions before deleting.

## Respond with htmx headers

The `htmx` package sets the response headers htmx 4 reads:

| Function | Header | Effect |
| --- | --- | --- |
| `Trigger(w, "saved")` | `HX-Trigger` | Dispatch events on the requesting element. |
| `TriggerDetail(w, "notify", detail)` | `HX-Trigger` | An event with a JSON detail. Calls accumulate. |
| `Redirect(w, "/notes/")` | `HX-Redirect` | Full-page navigation. Writes status 200. |
| `Location(w, "/notes/")` | `HX-Location` | htmx navigation without a full reload. Writes 200. |
| `Refresh(w)` | `HX-Refresh` | Reload the page. |
| `Retarget(w, "#errors")` | `HX-Retarget` | Swap into another element. |
| `Reswap(w, "outerHTML")` | `HX-Reswap` | Change the swap style. |
| `Reselect(w, "#list")` | `HX-Reselect` | Swap only part of the response. |
| `PushURL(w, "/notes/?page=2")` | `HX-Push-Url` | Push a history entry. |
| `ReplaceURL(w, "/notes/")` | `HX-Replace-Url` | Replace the current history entry. |

`Redirect`, `Location`, `PushURL` and `ReplaceURL` accept only local paths and
return an error otherwise, so a `?next=` value cannot redirect off-site. Use
`Redirect` instead of `http.Redirect` for htmx requests: the browser follows a
3xx itself, and htmx would swap the target page into the fragment.

```go
func create(w http.ResponseWriter, r *http.Request) {
    // ... validate and save ...
    if htmx.IsRequest(r) {
        _ = htmx.TriggerDetail(w, "notify", map[string]string{"message": "Saved"})
        _ = htmx.Redirect(w, "/notes/")
        return
    }
    http.Redirect(w, r, "/notes/", http.StatusSeeOther)
}
```

### Out-of-band swaps

Update other parts of the page in the same response. Group the main fragment with
elements that carry `hx-swap-oob` and a matching `id`:

```templ
templ counter(n int) {
    <span id="note-count" hx-swap-oob="true">{ strconv.Itoa(n) }</span>
}
```

```go
err := htmx.HTML(w, r, http.StatusOK, page, ui.Group(ui.Fragment(row(note)), ui.Fragment(counter(total))))
```

### Errors

With [error pages](https://cicadel.fauzaanu.com/en/dev/howto/error-pages/index.md) installed, a failed htmx partial request keeps
its status, swaps nothing (`HX-Reswap: none`) and triggers `cicadel:error` with
the status and title.

## htmx 4 attributes

| Need | Use |
| --- | --- |
| Inherit an attribute | Explicit inheritance, such as `hx-target:inherited="#results"` |
| Queue requests | `hx-sync="this:queue all"` |
| Keep content on a server error | `hx-status:5xx="swap:none"` |

Return useful field errors with status 422.
