htmx 4: explicit inheritance
htmx 4 inherits nothing by default: :inherited and :inherited:append declare a wrapper's reach, and the compiler warns where it was lost.
htmx 4: explicit inheritance
htmx 4 inherits no attribute unless the ancestor says so. This example
pins 4.0.0 in its own ghtmx.json and shows the two name modifiers
that replace htmx 2's implicit inheritance:
hx-target:inherited="#out"andhx-include:inherited="#team"on the panel reach every button beneath it;hx-include:inherited:append="#role"on the nested group extends the inherited value instead of replacing it.
Every button posts to the same bound route (hx-post={ Assign }); the
handler echoes the fields it received, so the differences between the
three responses are entirely what the markup around each button chose
to include.
ghtmx generate && go run ./cmd # serves http://127.0.0.1:8086/inherit
Remove :inherited from the panel's hx-target and run
ghtmx generate again: the compiler reports GHTMX-W0202, naming the
panel, the attribute, and the button that would no longer find its
target โ under htmx 2 that markup was valid, under htmx 4 it is valid
and silently wrong, which is exactly what the warning exists for. Pin
the project back to 2.0.10 and the same modifiers are reported as
introduced in 4.0.0 (GHTMX-E0501).
inheritance.ghtmx
package htmx4inheritance
import "github.com/go-monolith/ghtmx/examples/htmx4-inheritance/ghtmxgen"
templ page() {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>htmx 4: explicit inheritance</title>
@ghtmxgen.HTMXScript()
@styleSheet()
</head>
<body>
<main class="app">
<span class="badge">ghtmx example ยท htmx 4</span>
<h1>Explicit inheritance</h1>
<p class="tagline">htmx 4 inherits nothing by default. The panel below declares its swap target and its included field once with <code>:inherited</code>; the nested group extends the included fields with <code>:inherited:append</code>; the buttons carry only their bound <code>hx-post</code>.</p>
<div class="fields">
<label>team <input id="team" name="team" value="core" autocomplete="off"/></label>
<label>role <input id="role" name="role" value="admin" autocomplete="off"/></label>
</div>
<section class="panel" hx-target:inherited="#out" hx-include:inherited="#team">
<p class="hint"><code>hx-target:inherited="#out" hx-include:inherited="#team"</code></p>
<button hx-post={ Assign }>Assign โ sends team</button>
<div class="panel nested" hx-include:inherited:append="#role">
<p class="hint"><code>hx-include:inherited:append="#role"</code></p>
<button hx-post={ Assign }>Assign โ sends team and role</button>
</div>
</section>
<p class="hint">Outside the panel nothing is inherited, so this button names its own target and includes nothing:</p>
<button hx-post={ Assign } hx-target="#out">Assign โ sends nothing</button>
<div id="out" class="out">
<p class="muted">Press a button: the handler echoes the fields it received.</p>
</div>
</main>
</body>
</html>
}
// received is what every Assign response swaps into #out.
fragment received(fields []Field) {
if len(fields) == 0 {
<p class="muted">received no fields</p>
} else {
<ul class="received">
for _, f := range fields {
<li><code>{ f.Name }</code> = { f.Value }</li>
}
</ul>
}
}
ghtmx.json
{
"htmxVersion": "4.0.0",
"generatedPackage": { "dir": "examples/htmx4-inheritance/ghtmxgen" }
}
inheritance.go
// The htmx4-inheritance example: htmx 4 inherits no attribute unless the
// ancestor says so. A panel declares the swap target and the included
// field once with :inherited, a nested group extends the included
// fields with :inherited:append, and the buttons beneath carry nothing
// but their bound hx-post. The compiler validates the modifiers against
// the 4.0.0 pin in ghtmx.json and reports a wrapper that silently lost
// its reach (GHTMX-W0202) when :inherited is dropped.
//
// Run it with:
//
// ghtmx generate && go run ./cmd
package htmx4inheritance
import (
_ "embed"
"net/http"
"sort"
"github.com/go-monolith/ghtmx"
)
//go:embed inheritance.css
var styleCSS string
// styleSheet inlines inheritance.css into the page head. The rules live
// in their own file so the template shows markup, not presentation.
func styleSheet() ghtmx.Component {
return ghtmx.Raw("<style>" + styleCSS + "</style>")
}
// Field is one form value the assign handler received.
type Field struct {
Name string
Value string
}
func inheritHome(w http.ResponseWriter, r *http.Request) {
if err := page().Render(r.Context(), w); err != nil {
http.Error(w, "failed to render", http.StatusInternalServerError)
}
}
// Assign echoes which fields arrived: the same handler serves every
// button, so the differences between responses are entirely what the
// markup around each button chose to include.
func Assign(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "bad form", http.StatusBadRequest)
return
}
fields := make([]Field, 0, len(r.PostForm))
for name, values := range r.PostForm {
if len(values) > 0 {
fields = append(fields, Field{Name: name, Value: values[0]})
}
}
sort.Slice(fields, func(i, j int) bool { return fields[i].Name < fields[j].Name })
if err := receivedFragment(fields).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 /inherit", inheritHome)
mux.HandleFunc("POST /inherit/assign", Assign)
return mux
}
inheritance.css
: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(40rem, 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; }
.fields { display: flex; gap: 1rem; margin-bottom: 1rem; }
.fields label { flex: 1; display: flex; flex-direction: column; gap: .2rem; font-size: .85rem; color: var(--muted); }
input {
font-size: 1rem; padding: .45rem .7rem; border-radius: .45rem;
border: 1px solid var(--border); background: var(--bg); color: inherit;
}
.panel { border: 1px dashed var(--primary); border-radius: .5rem; padding: .8rem 1rem; margin: .6rem 0; }
.panel.nested { border-style: solid; margin-top: .8rem; }
.hint { margin: 0 0 .5rem; font-size: .85rem; color: var(--muted); }
button {
font-size: .9rem; padding: .45rem .9rem; border-radius: .45rem; cursor: pointer;
border: 1px solid var(--border); background: var(--bg); color: inherit;
}
button:hover { border-color: var(--primary); color: var(--primary); }
.out { margin-top: 1rem; padding: .8rem 1rem; border-radius: .5rem; background: var(--bg); min-height: 3rem; }
.out p, .out ul { margin: 0; }
.received { padding-left: 1.2rem; }
.muted { color: var(--muted); }
cmd/main.go
// Command htmx4inheritance serves the htmx4-inheritance example standalone.
package main
import (
"fmt"
"net/http"
"os"
example "github.com/go-monolith/ghtmx/examples/htmx4-inheritance"
)
func main() {
addr := "127.0.0.1:8086"
if v := os.Getenv("GHTMX_EXAMPLE_ADDR"); v != "" {
addr = v
}
fmt.Printf("Listening on http://%s/inherit\n", addr)
if err := http.ListenAndServe(addr, example.Routes()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}