Fragments

Compile-time fragments rendered inline in a page and standalone for htmx swaps, byte-identically.

Live demo ↗

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)
	}
}

All examples