Add interactive behavior ¶
Use htmx to update part of a page with server-rendered HTML. This example adds a search form that works both with htmx and with ordinary navigation.
Ask your agent ¶
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.Register the script and search routes ¶
Add apps/search/app.go. The bundled script is served by your Go application; no CDN is needed.
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)
}
}
}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.
Include the script and render results ¶
Add apps/search/page.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.
Register the app and build ¶
Import example.com/myapp/apps/search in project/apps.go and add search.New() to the existing list:
func InstalledApps(_ *gorm.DB) ([]cicadel.App, error) {
return []cicadel.App{
notes.New(),
search.New(),
}, nil
}Keep any other app registrations. Use your own module path in the import.
go tool cicadel ui build
go tool cicadel ui check --json
go test -race ./...
go run manage.go runserverOpen http://127.0.0.1:8000/search. You can also check both response forms:
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'The first response is a full HTML document. The second contains <ul> results without a document wrapper. htmx.HTML also handles the required Vary headers and 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 include its CSRF token:
<form action="/notes/delete" method="post">
@form.CSRF.Call(form.CSRFProps{Token: token}, ui.NoSlots{})
<button hx-delete="/notes/42" hx-include="closest form" hx-target="#notes">
Delete note
</button>
</form>Here form is github.com/fauzaanu/cicadel/ui/form, and token comes from your session’s CSRF middleware. Implement both routes if you want the POST fallback. Check object permissions before deleting; htmx headers are not authorization.
Cicadel bundles htmx 4. Use explicit inheritance such as hx-target:inherited="#results", hx-sync="this:queue all" for queues, and hx-status:5xx="swap:none" when an error response should not replace content. Return useful field errors with status 422.