Fragments
Compile-time fragments rendered inline in a page and standalone for htmx swaps, byte-identically.
rows.ghtmx
package fragments
import "github.com/go-monolith/ghtmx/ghtmxgen"
// ItemRow is declared once and referenced by two pages. The compiler emits
// one shared body plus two entry points over it: ItemRow (inline component)
// and ItemRowFragment (standalone render for htmx swaps) — FR-030, FR-031.
fragment ItemRow(id string, name string) {
<tr id={ "item-" + id }>
<td>{ name }</td>
<td>
<a class="action" hx-get={ ghtmxgen.RowDetail(id) } hx-target={ "#item-" + id } hx-swap="outerHTML">refresh</a>
</td>
</tr>
}
templ listPage(items []Item) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>fragments</title>
@ghtmxgen.HTMXScript()
<style>
:root { --primary: #008391; --bg: #f5f6f7; --surface: #ffffff; --text: #1c1e21; --muted: #525860; --border: #dadde1; color-scheme: light dark; }
@media (prefers-color-scheme: dark) {
:root { --primary: #dbbc30; --bg: #1b1b1d; --surface: #242526; --text: #e3e3e3; --muted: #a5adba; --border: #444950; }
}
* { box-sizing: border-box; }
body {
font-family: system-ui, sans-serif; margin: 0; min-height: 100vh;
background: var(--bg); color: var(--text);
display: flex; justify-content: center; align-items: flex-start;
}
.app {
width: min(36rem, 92vw); margin: 3rem 0; padding: 1.6rem 1.8rem;
background: var(--surface); border: 1px solid var(--border);
border-radius: .6rem; box-shadow: 0 8px 24px rgba(0, 0, 0, .1);
}
.badge {
display: inline-block; font-size: .72rem; font-weight: 600;
color: var(--primary); border: 1px solid var(--primary);
border-radius: 999px; padding: .1rem .6rem; margin-bottom: .8rem;
}
h1 { margin: 0 0 .4rem; font-size: 1.5rem; }
.tagline { margin: 0 0 1.2rem; color: var(--muted); font-size: .95rem; }
button, a.action {
font-size: .9rem; padding: .45rem .9rem; border-radius: .45rem; cursor: pointer;
border: 1px solid var(--border); background: var(--bg); color: inherit;
text-decoration: none; display: inline-block;
}
button:hover, a.action:hover { border-color: var(--primary); color: var(--primary); }
table { width: 100%; border-collapse: collapse; margin: .6rem 0 1rem; }
td { padding: .5rem .3rem; border-bottom: 1px solid var(--border); }
tr:last-child td { border-bottom: none; }
td:last-child { text-align: right; }
</style>
</head>
<body>
<main class="app">
<span class="badge">ghtmx example</span>
<h1>Fragments</h1>
<p class="tagline">Each row is one compile-time fragment: embedded inline in this page and served standalone for the refresh swaps, byte-identically.</p>
<table id="items">
<tbody>
for _, it := range items {
@ItemRow(it.ID, it.Name)
}
</tbody>
</table>
<button hx-get={ fragmentsHome } hx-target="#items" hx-select="#items" hx-swap="outerHTML">Reload</button>
</main>
</body>
</html>
}
templ soloPage(it Item) {
<div id="solo">
<table>
<tbody>
@ItemRow(it.ID, it.Name)
</tbody>
</table>
</div>
}
fragments.go
// The fragments example: one fragment declaration serves both render
// modes. Pages compose it inline via @ItemRow(...); the row's refresh link
// binds a typed route constructor whose handler renders only the fragment
// via ItemRowFragment(...).RenderFragment — no page around it (FR-031,
// FR-034).
package fragments
import (
"net/http"
)
// Item is a row of the demo table.
type Item struct {
ID string
Name string
}
var items = []Item{
{ID: "1", Name: "Alpha"},
{ID: "2", Name: "Beta"},
{ID: "3", Name: "Gamma"},
}
func itemByID(id string) (Item, bool) {
for _, it := range items {
if it.ID == id {
return it, true
}
}
return Item{}, false
}
func fragmentsHome(w http.ResponseWriter, r *http.Request) {
if err := listPage(items).Render(r.Context(), w); err != nil {
http.Error(w, "failed to render", http.StatusInternalServerError)
}
}
// rowDetail answers the row's hx-get swap with only the fragment's markup.
func rowDetail(w http.ResponseWriter, r *http.Request) {
it, ok := itemByID(r.PathValue("id"))
if !ok {
http.NotFound(w, r)
return
}
if err := ItemRowFragment(it.ID, it.Name).RenderFragment(r.Context(), w); err != nil {
http.Error(w, "failed to render", http.StatusInternalServerError)
}
}
// Routes builds the example's router; the official docs site mounts
// it as a live demo.
func Routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /fragments", fragmentsHome)
mux.HandleFunc("GET /fragments/rows/{id}", rowDetail)
return mux
}
cmd/main.go
// Command fragments serves the fragments example standalone.
package main
import (
"fmt"
"net/http"
"os"
example "github.com/go-monolith/ghtmx/examples/fragments"
)
func main() {
addr := "127.0.0.1:8082"
if v := os.Getenv("GHTMX_EXAMPLE_ADDR"); v != "" {
addr = v
}
fmt.Printf("Listening on http://%s/fragments\n", addr)
if err := http.ListenAndServe(addr, example.Routes()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}