Below are a set of UX patterns implemented in htmx with minimal HTML and styling.
@@ -32,13 +42,13 @@ templ page() {
- @exampleRow(
- "/examples/click-to-edit",
+ @e.exampleRow(
+ "/examples/templ/click-to-edit",
"Click To Edit",
"Demonstrates inline editing of a data object",
)
- @exampleRow(
- "/examples/bulk-update",
+ @e.exampleRow(
+ "/examples/templ/bulk-update",
"Bulk Update",
"Demonstrates bulk updating of multiple rows of data",
)
@@ -47,7 +57,7 @@ templ page() {
}
}
-templ exampleRow(link, name, description string) {
+templ (e templExample) exampleRow(link, name, description string) {
{ name }
@@ -57,3 +67,17 @@ templ exampleRow(link, name, description string) {
}
+
+templ (e templExample) notFoundPage() {
+ @e.layout.Base("Not Found") {
+ Not Found
+ The page you are looking for does not exist.
+ }
+}
+
+templ (e templExample) serverErrorPage(err string) {
+ @e.layout.Base("Server Error") {
+ Something went wrong
+ { err }
+ }
+}
diff --git a/examples/web/examples/examples_templ.go b/examples/web/examples/examples_templ.go
new file mode 100644
index 0000000..d19e7b1
--- /dev/null
+++ b/examples/web/examples/examples_templ.go
@@ -0,0 +1,235 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.2.543
+package examples
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import (
+ "bytes"
+ "context"
+ "io"
+
+ "github.com/a-h/templ"
+
+ "github.com/will-wow/typed-htmx-go/examples/web/layout"
+)
+
+type templExample struct {
+ layout layout.Templ
+}
+
+func newTemplExample() templExample {
+ return templExample{
+ layout: layout.Templ{},
+ }
+}
+
+func (e templExample) page() templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("UI Templ Examples Below are a set of UX patterns implemented in htmx with minimal HTML and styling.
These are ported from the htmx examples and are intended showcase the use of hx
when building HTMX applications.
You can copy and paste them and then adjust them for your needs.
Pattern Description ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = e.exampleRow(
+ "/examples/templ/click-to-edit",
+ "Click To Edit",
+ "Demonstrates inline editing of a data object",
+ ).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = e.exampleRow(
+ "/examples/templ/bulk-update",
+ "Bulk Update",
+ "Demonstrates bulk updating of multiple rows of data",
+ ).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+ }
+ return templ_7745c5c3_Err
+ })
+ templ_7745c5c3_Err = e.layout.Base("").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func (e templExample) exampleRow(link, name, description string) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var3 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var3 == nil {
+ templ_7745c5c3_Var3 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(name)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/examples.templ`, Line: 62, Col: 41}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(description)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/examples.templ`, Line: 65, Col: 16}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func (e templExample) notFoundPage() templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var7 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var7 == nil {
+ templ_7745c5c3_Var7 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var8 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Not Found The page you are looking for does not exist.
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+ }
+ return templ_7745c5c3_Err
+ })
+ templ_7745c5c3_Err = e.layout.Base("Not Found").Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
+
+func (e templExample) serverErrorPage(err string) templ.Component {
+ return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var9 == nil {
+ templ_7745c5c3_Var9 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var10 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
+ if !templ_7745c5c3_IsBuffer {
+ templ_7745c5c3_Buffer = templ.GetBuffer()
+ defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Something went wrong ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var11 string
+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(err)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/examples.templ`, Line: 80, Col: 10}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
+ }
+ return templ_7745c5c3_Err
+ })
+ templ_7745c5c3_Err = e.layout.Base("Server Error").Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if !templ_7745c5c3_IsBuffer {
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
+ }
+ return templ_7745c5c3_Err
+ })
+}
diff --git a/examples/web/layout/layout.gom.go b/examples/web/layout/layout.gom.go
new file mode 100644
index 0000000..d9fd072
--- /dev/null
+++ b/examples/web/layout/layout.gom.go
@@ -0,0 +1,102 @@
+package layout
+
+import (
+ g "github.com/maragudk/gomponents"
+ . "github.com/maragudk/gomponents/html"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
+)
+
+var ghx = htmx.NewGomponents()
+
+type Gomponents struct{}
+
+func (l Gomponents) Base(title string, children ...g.Node) g.Node {
+ return Doctype(
+ HTML(
+ Lang("en"),
+ Head(
+ Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
+ Meta(Charset("utf-8")),
+
+ g.If(title != "",
+ TitleEl(g.Textf("%s | HX | Examples", title)),
+ ),
+ g.If(title == "",
+ TitleEl(g.Text("HX | Examples")),
+ ),
+
+ Meta(Name("htmx-config"), Content(`{"includeIndicatorStyles":false}`)),
+ Meta(Name("color-scheme"), Content("light")),
+ Meta(Name("description"), Content("Examples of typed-htmx-go/hx")),
+ Meta(Name("referrer"), Content("origin-when-cross-origin")),
+ Meta(Name("creator"), Content("Will Ockelmann-Wagner")),
+ Script(Src("https://unpkg.com/htmx.org@1.9.10")),
+ Link(Rel("stylesheet"), Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css")),
+ Link(Rel("stylesheet"), Href("/static/main.css")),
+ ),
+ Body(
+ ghx.Boost(true),
+ Main(
+ l.nav(),
+ g.Group(children),
+ ),
+ ),
+ ),
+ )
+}
+
+func (l Gomponents) nav() g.Node {
+ return Nav(
+ Ul(
+ Li(
+ A(
+ Href("/examples/gomponents/"),
+ Strong(
+ g.Text("T"),
+ U(g.Text("HX")),
+ g.Text("GO"),
+ ),
+ ),
+ ),
+ ),
+ Ul(
+ Li(
+ A(
+ Href("https://pkg.go.dev/github.com/will-wow/typed-htmx-go/hx"),
+ Target("_blank"),
+ Rel("noopener"),
+ g.Text("Docs"),
+ ),
+ ),
+ Li(
+ A(
+ Href("https://htmx.org"),
+ Target("_blank"),
+ Rel("noopener"),
+ g.Text("HTMX"),
+ ),
+ ),
+ Li(
+ A(
+ Href("/"),
+ g.Text("Templ"),
+ ),
+ ),
+ Li(
+ A(
+ Href("/examples/gomponents/"),
+ g.Text("Gomponents"),
+ ),
+ ),
+ Li(
+ A(
+ Href("https://github.com/will-wow/typed-htmx-go"),
+ Target("_blank"),
+ Rel("noopener"),
+ g.Text("GitHub"),
+ ),
+ ),
+ ),
+ )
+}
diff --git a/examples/templ/web/layout/layout.templ b/examples/web/layout/layout.templ
similarity index 80%
rename from examples/templ/web/layout/layout.templ
rename to examples/web/layout/layout.templ
index 5d372fc..61c85f4 100644
--- a/examples/templ/web/layout/layout.templ
+++ b/examples/web/layout/layout.templ
@@ -1,19 +1,23 @@
package layout
import (
- "github.com/will-wow/typed-htmx-go/hx"
+ "github.com/will-wow/typed-htmx-go/htmx"
)
-templ Base(title string, className ...string) {
+var thx = htmx.NewTempl()
+
+type Templ struct{}
+
+templ (l Templ) Base(title string, className ...string) {
if title == "" {
- HX | Examples
+ Templ | HX | Examples
} else {
- { title } | HX | Examples
+ { title } | Templ | HX | Examples
}
@@ -27,11 +31,7 @@ templ Base(title string, className ...string) {
/>
-
+
@nav()
{ children... }
@@ -71,7 +71,12 @@ templ nav() {
- Examples
+ Templ
+
+
+
+
+ Gomponents
diff --git a/examples/templ/web/layout/layout_templ.go b/examples/web/layout/layout_templ.go
similarity index 90%
rename from examples/templ/web/layout/layout_templ.go
rename to examples/web/layout/layout_templ.go
index 2107492..d5eb438 100644
--- a/examples/templ/web/layout/layout_templ.go
+++ b/examples/web/layout/layout_templ.go
@@ -11,10 +11,15 @@ import (
"io"
"github.com/a-h/templ"
- "github.com/will-wow/typed-htmx-go/hx"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
)
-func Base(title string, className ...string) templ.Component {
+var thx = htmx.NewTempl()
+
+type Templ struct{}
+
+func (l Templ) Base(title string, className ...string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
@@ -32,7 +37,7 @@ func Base(title string, className ...string) templ.Component {
return templ_7745c5c3_Err
}
if title == "" {
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("HX | Examples ")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Templ | HX | Examples ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -44,13 +49,13 @@ func Base(title string, className ...string) templ.Component {
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/layout/layout.templ`, Line: 15, Col: 18}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/layout/layout.templ`, Line: 19, Col: 18}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | HX | Examples")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" | Templ | HX | Examples")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -59,9 +64,7 @@ func Base(title string, className ...string) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, hx.New().
- Boost(true).
- Build())
+ templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, thx.Boost(true))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -118,7 +121,7 @@ func nav() templ.Component {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/examples/templ/web/static/main.css b/examples/web/static/main.css
similarity index 100%
rename from examples/templ/web/static/main.css
rename to examples/web/static/main.css
diff --git a/examples/templ/web/static/main.js b/examples/web/static/main.js
similarity index 100%
rename from examples/templ/web/static/main.js
rename to examples/web/static/main.js
diff --git a/examples/templ/web/ui/ui.go b/examples/web/ui/ui.go
similarity index 100%
rename from examples/templ/web/ui/ui.go
rename to examples/web/ui/ui.go
diff --git a/examples/templ/web/web.go b/examples/web/web.go
similarity index 66%
rename from examples/templ/web/web.go
rename to examples/web/web.go
index c8db704..ccc3ab5 100644
--- a/examples/templ/web/web.go
+++ b/examples/web/web.go
@@ -7,9 +7,9 @@ import (
"net/http"
"os"
- "github.com/will-wow/typed-htmx-go/examples/templ/web/bulkupdate"
- "github.com/will-wow/typed-htmx-go/examples/templ/web/clicktoedit"
- "github.com/will-wow/typed-htmx-go/examples/templ/web/examples"
+ "github.com/will-wow/typed-htmx-go/examples/web/bulkupdate"
+ "github.com/will-wow/typed-htmx-go/examples/web/clicktoedit"
+ "github.com/will-wow/typed-htmx-go/examples/web/examples"
)
//go:embed "static"
@@ -29,19 +29,25 @@ func NewHttpHandler() http.Handler {
return handler.routes()
}
+var templIndexRoutes = examples.NewRoutes(false)
+var gomIndexRoutes = examples.NewRoutes(true)
+
func (h *Handler) routes() http.Handler {
mux := http.NewServeMux()
// Catch-all
- mux.HandleFunc("/", notFound)
+ mux.HandleFunc("/", templIndexRoutes.NewNotFoundHandler)
// Set up a in-memory file server for the embedded static files.
fileServer := http.FileServerFS(staticFiles)
mux.Handle("GET /static/", fileServer)
- mux.HandleFunc("/{$}", examples.Handler)
- delegateExample(mux, "/examples/click-to-edit", clicktoedit.Handler())
- delegateExample(mux, "/examples/bulk-update", bulkupdate.Handler())
+ mux.HandleFunc("/{$}", templIndexRoutes.NewIndexHandler)
+ mux.HandleFunc("/examples/gomponents/{$}", gomIndexRoutes.NewIndexHandler)
+ delegateExample(mux, "/examples/templ/click-to-edit", clicktoedit.NewHandler(false))
+ delegateExample(mux, "/examples/gomponents/click-to-edit", clicktoedit.NewHandler(true))
+ delegateExample(mux, "/examples/templ/bulk-update", bulkupdate.NewHandler(false))
+ delegateExample(mux, "/examples/gomponents/bulk-update", bulkupdate.NewHandler(true))
return h.recoverPanic(h.logRequest(mux))
}
@@ -65,12 +71,6 @@ func (h *Handler) logRequest(next http.Handler) http.Handler {
})
}
-func notFound(w http.ResponseWriter, r *http.Request) {
- component := notFoundPage()
- w.WriteHeader(http.StatusNotFound)
- _ = component.Render(r.Context(), w)
-}
-
func (h *Handler) recoverPanic(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create a deferred function (which will always be run in the event
@@ -81,7 +81,7 @@ func (h *Handler) recoverPanic(next http.Handler) http.Handler {
if err := recover(); err != nil {
// Set a "Connection: close" header on the response.
w.Header().Set("Connection", "close")
- component := serverErrorPage(fmt.Sprintf("%s", err))
+ component := examples.ServerErrorPage(fmt.Sprintf("%s", err))
w.WriteHeader(http.StatusInternalServerError)
_ = component.Render(r.Context(), w)
}
diff --git a/go.mod b/go.mod
index ee6fe21..a05c3f9 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,7 @@
module github.com/will-wow/typed-htmx-go
go 1.21
+
+require github.com/a-h/templ v0.2.543
+
+require github.com/maragudk/gomponents v0.20.2
diff --git a/go.sum b/go.sum
index e69de29..f7146ef 100644
--- a/go.sum
+++ b/go.sum
@@ -0,0 +1,6 @@
+github.com/a-h/templ v0.2.543 h1:8YyLvyUtf0/IE2nIwZ62Z/m2o2NqwhnMynzOL78Lzbk=
+github.com/a-h/templ v0.2.543/go.mod h1:jP908DQCwI08IrnTalhzSEH9WJqG/Q94+EODQcJGFUA=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/maragudk/gomponents v0.20.2 h1:39FhnBNNCJzqNcD9Hmvp/5xj0otweFoyvVgFG6kXoy0=
+github.com/maragudk/gomponents v0.20.2/go.mod h1:nHkNnZL6ODgMBeJhrZjkMHVvNdoYsfmpKB2/hjdQ0Hg=
diff --git a/go.work b/go.work
index 7903165..0397503 100644
--- a/go.work
+++ b/go.work
@@ -2,5 +2,5 @@ go 1.22.0
use (
.
- ./examples/templ
+ ./examples
)
diff --git a/go.work.sum b/go.work.sum
index 1ecc672..07d7ca8 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -1,5 +1,7 @@
+github.com/a-h/htmlformat v0.0.0-20231108124658-5bd994fe268e/go.mod h1:FMIm5afKmEfarNbIXOaPHFY8X7fo+fRQB6I9MPG2nB0=
github.com/a-h/lexical v0.0.53/go.mod h1:d73jw5cgKXuYypRozNBuxRNFrTWQ3y5hVMG7rUjh1Qw=
github.com/a-h/parse v0.0.0-20230402144745-e6c8bc86e846/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
+github.com/a-h/pathvars v0.0.12/go.mod h1:7rLTtvDVyKneR/N65hC0lh2sZ2KRyAmWFaOvv00uxb0=
github.com/a-h/protocol v0.0.0-20230224160810-b4eec67c1c22/go.mod h1:Gm0KywveHnkiIhqFSMZglXwWZRQICg3KDWLYdglv/d8=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI=
@@ -7,6 +9,7 @@ github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4Nij
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
+github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/segmentio/encoding v0.3.6/go.mod h1:n0JeuIqEQrQoPDGsjo8UNd1iA0U8d8+oHAA4E3G3OxM=
go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac=
diff --git a/htmx/gomponents.go b/htmx/gomponents.go
new file mode 100644
index 0000000..4d345b9
--- /dev/null
+++ b/htmx/gomponents.go
@@ -0,0 +1,52 @@
+package htmx
+
+import (
+ "io"
+ "strings"
+ "text/template"
+
+ g "github.com/maragudk/gomponents"
+)
+
+func NewGomponents() HX[GomponentsAttrs] {
+ return NewHX(
+ func(key Attribute, value any) GomponentsAttrs {
+ return GomponentsAttrs{key: key, value: value}
+ },
+ )
+}
+
+type GomponentsAttrs struct {
+ key Attribute
+ value any
+}
+
+var _ g.Node = GomponentsAttrs{key: "", value: false}
+
+func (a GomponentsAttrs) Render(w io.Writer) error {
+ switch v := a.value.(type) {
+ // For strings, print the key='value' pair.
+ case string:
+ _, err := w.Write([]byte(" " + string(a.key) + `="` + template.HTMLEscapeString(v) + `"`))
+ return err
+ // For booleans, print just the key if true.
+ case bool:
+ if v {
+ _, err := w.Write([]byte(" " + string(a.key)))
+ return err
+ }
+ }
+ return nil
+}
+
+// Type satisfies nodeTypeDescriber.
+func (n GomponentsAttrs) Type() g.NodeType {
+ return g.AttributeType
+}
+
+// String satisfies fmt.Stringer.
+func (n GomponentsAttrs) String() string {
+ var b strings.Builder
+ _ = n.Render(&b)
+ return b.String()
+}
diff --git a/htmx/gomponents_test.go b/htmx/gomponents_test.go
new file mode 100644
index 0000000..5a34882
--- /dev/null
+++ b/htmx/gomponents_test.go
@@ -0,0 +1,43 @@
+package htmx_test
+
+import (
+ "os"
+ "testing"
+
+ g "github.com/maragudk/gomponents"
+ . "github.com/maragudk/gomponents/html"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
+ "github.com/will-wow/typed-htmx-go/htmx/swap"
+)
+
+var gomHx = htmx.NewGomponents()
+
+func BenchmarkBoost(b *testing.B) {
+ attrs := make([]htmx.GomponentsAttrs, b.N)
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ attrs[i] = gomHx.Boost(true)
+ }
+}
+
+func ExampleNewGomponents() {
+ hx := htmx.NewGomponents()
+
+ component := FormEl(
+ hx.Boost(true),
+ hx.Post("/submit"),
+ hx.Swap(swap.OuterHTML),
+ ID("form"),
+ Input(
+ Name("firstName"),
+ ),
+ Button(
+ Type("submit"),
+ g.Text("Submit"),
+ ),
+ )
+
+ _ = component.Render(os.Stdout)
+ // Output:
+}
diff --git a/hx/hx.go b/htmx/htmx.go
similarity index 87%
rename from hx/hx.go
rename to htmx/htmx.go
index f19c792..109ade7 100644
--- a/hx/hx.go
+++ b/htmx/htmx.go
@@ -1,8 +1,10 @@
-// package htmx provides well-documented Go functions for building HTMX attributes.
+// package base provides Go functions for building HTMX attributes.
+// These should usually be used through a proxy like templ/hx or gomponents/hx.
+//
// See [typed-htmx-go.vercel.app] for example usage.
//
// [typed-htmx-go.vercel.app]: https://typed-htmx-go.vercel.app/
-package hx
+package htmx
import (
"encoding/json"
@@ -12,53 +14,23 @@ import (
"strings"
"time"
- "github.com/will-wow/typed-htmx-go/hx/swap"
- "github.com/will-wow/typed-htmx-go/hx/trigger"
+ "github.com/will-wow/typed-htmx-go/htmx/swap"
+ "github.com/will-wow/typed-htmx-go/htmx/trigger"
)
+type NewAttr[T any] func(key Attribute, value any) T
+
// An HX constructs HTMX attributes.
-type HX struct {
- attrs map[string]any
+type HX[T any] struct {
+ attr NewAttr[T]
}
-// New starts a new HTMX attributes builder.
-// Methods support HTMX v1.9.10.
-func New() *HX {
- return &HX{
- attrs: map[string]any{},
+func NewHX[T any](attr NewAttr[T]) HX[T] {
+ return HX[T]{
+ attr: attr,
}
}
-// Build returns the final attribute map, compatible with [templ.Attributes].
-func (hx *HX) Build() map[string]any {
- return hx.attrs
-}
-
-// String renders the attributes as HTML attributes.
-func (hx *HX) String() string {
- attributes := make([]string, 0, len(hx.attrs))
-
- for k, v := range hx.attrs {
- switch v := v.(type) {
- // For strings, print the key='value' pair.
- case string:
- attributes = append(attributes, fmt.Sprintf(`%s='%v'`, k, v))
- // For booleans, print just the key if true.
- case bool:
- if v {
- attributes = append(attributes, k)
- }
- }
- }
-
- // Sort, which makes testing easier.
- if len(attributes) > 1 {
- slices.Sort(attributes)
- }
-
- return strings.Join(attributes, " ")
-}
-
// A StandardCSSSelector is any valid CSS selector, like #element or `.class > button`.
type StandardCSSSelector string
@@ -97,13 +69,11 @@ type StandardCSSSelector string
//
// [hx-boost]: https://htmx.org/attributes/hx-boost/
// [nice fallback]: https://en.wikipedia.org/wiki/Progressive_enhancement
-func (hx *HX) Boost(boost bool) *HX {
+func (hx *HX[T]) Boost(boost bool) T {
if boost {
- hx.set(Boost, "true")
- } else {
- hx.set(Boost, "false")
+ return hx.attr("hx-boost", "true")
}
- return hx
+ return hx.attr("hx-boost", "false")
}
// Get will cause an element to issue a GET to the specified URL and swap the HTML into the DOM using a swap strategy.
@@ -124,8 +94,8 @@ func (hx *HX) Boost(boost bool) *HX {
//
// [hx-get]: https://htmx.org/attributes/hx-get/
// [Parameters]: https://htmx.org/docs/#parameters
-func (hx *HX) Get(url string) *HX {
- return hx.set(Get, url)
+func (hx *HX[T]) Get(url string) T {
+ return hx.attr(Get, url)
}
// Post will cause an element to issue a POST to the specified URL and swap the HTML into the DOM using a swap strategy.
@@ -147,8 +117,8 @@ func (hx *HX) Get(url string) *HX {
//
// [hx-post]: https://htmx.org/attributes/hx-post/
// [Parameters]: https://htmx.org/docs/#parameters
-func (hx *HX) Post(url string) *HX {
- return hx.set(Post, url)
+func (hx *HX[T]) Post(url string) T {
+ return hx.attr(Post, url)
}
// On allows you to embed scripts inline to respond to events directly on an element; similar to the onevent properties found in HTML, such as onClick.
@@ -173,8 +143,8 @@ func (hx *HX) Post(url string) *HX {
// HTMX Attribute: [hx-on]
//
// [hx-on]: https://htmx.org/attributes/hx-on/
-func (hx *HX) On(event string, action string) *HX {
- return hx.setOther(fmt.Sprintf("hx-on:%s", event), action)
+func (hx *HX[T]) On(event string, action string) T {
+ return hx.attr(Attribute(fmt.Sprintf("hx-on:%s", event)), action)
}
// OnHTMX allows you to embed scripts inline to respond to HTMX events directly on an element; similar to the onevent properties found in HTML, such as onClick.
@@ -205,8 +175,8 @@ func (hx *HX) On(event string, action string) *HX {
// HTMX Attribute: [hx-on]
//
// [hx-on]: https://htmx.org/attributes/hx-on/
-func (hx *HX) OnHTMX(event string, action string) *HX {
- return hx.setOther(fmt.Sprintf("hx-on::%s", event), action)
+func (hx *HX[T]) OnHTMX(event string, action string) T {
+ return hx.attr(Attribute(fmt.Sprintf("hx-on::%s", event)), action)
}
// PushURL allows you to push a URL into the browser location history. This creates a new history entry, allowing navigation with the browser’s back and forward buttons. htmx snapshots the current DOM and saves it into its history cache, and restores from this cache on navigation.
@@ -232,8 +202,8 @@ func (hx *HX) OnHTMX(event string, action string) *HX {
// HTMX Attribute: [hx-push-url]
//
// [hx-push-url]: https://htmx.org/attributes/hx-push-url/
-func (hx *HX) PushURL(on bool) *HX {
- return hx.set(PushURL, boolToString(on))
+func (hx *HX[T]) PushURL(on bool) T {
+ return hx.attr(PushURL, boolToString(on))
}
// PushURLPath allows you to push a URL into the browser location history. This creates a new history entry, allowing navigation with the browser’s back and forward buttons. htmx snapshots the current DOM and saves it into its history cache, and restores from this cache on navigation.
@@ -257,8 +227,8 @@ func (hx *HX) PushURL(on bool) *HX {
// HTMX Attribute: [hx-push-url]
//
// [hx-push-url]: https://htmx.org/attributes/hx-push-url/
-func (hx *HX) PushURLPath(url string) *HX {
- return hx.set(PushURL, url)
+func (hx *HX[T]) PushURLPath(url string) T {
+ return hx.attr(PushURL, url)
}
// Select allows you to select the content you want swapped from a response. The value of this attribute is a CSS query selector of the element or elements to select from the response.
@@ -280,8 +250,8 @@ func (hx *HX) PushURLPath(url string) *HX {
// HTMX Attribute: [hx-select]
//
// [hx-select]: https://htmx.org/attributes/hx-select/
-func (hx *HX) Select(selector StandardCSSSelector) *HX {
- return hx.set(Select, string(selector))
+func (hx *HX[T]) Select(selector StandardCSSSelector) T {
+ return hx.attr(Select, string(selector))
}
// SelectOOB allows you to select content from a response to be swapped in via an out-of-band swap.
@@ -330,8 +300,8 @@ func (hx *HX) Select(selector StandardCSSSelector) *HX {
// HTMX Attribute: [hx-select-oob]
//
// [hx-select-oob]: https://htmx.org/attributes/hx-select-oob/
-func (hx *HX) SelectOOB(selectors ...StandardCSSSelector) *HX {
- return hx.set(SelectOOB, joinStringLikes(selectors, ","))
+func (hx *HX[T]) SelectOOB(selectors ...StandardCSSSelector) T {
+ return hx.attr(SelectOOB, joinStringLikes(selectors, ","))
}
type SelectOOBStrategy struct {
@@ -370,7 +340,7 @@ type SelectOOBStrategy struct {
// HTMX Attribute: [hx-select-oob]
//
// [hx-select-oob]: https://htmx.org/attributes/hx-select-oob
-func (hx *HX) SelectOOBWithStrategy(selectors ...SelectOOBStrategy) *HX {
+func (hx *HX[T]) SelectOOBWithStrategy(selectors ...SelectOOBStrategy) T {
values := make([]string, len(selectors))
for i, s := range selectors {
if s.Strategy == "" {
@@ -380,7 +350,7 @@ func (hx *HX) SelectOOBWithStrategy(selectors ...SelectOOBStrategy) *HX {
}
}
- return hx.set(SelectOOB, strings.Join(values, ","))
+ return hx.attr(SelectOOB, strings.Join(values, ","))
}
// Swap allows you to specify how the response will be swapped in relative to the target of an AJAX request.
@@ -398,8 +368,8 @@ func (hx *HX) SelectOOBWithStrategy(selectors ...SelectOOBStrategy) *HX {
// HTMX Attribute: [hx-swap]
//
// [hx-swap]: https://htmx.org/attributes/hx-swap
-func (hx *HX) Swap(strategy swap.Strategy) *HX {
- return hx.set(Swap, string(strategy))
+func (hx *HX[T]) Swap(strategy swap.Strategy) T {
+ return hx.attr(Swap, string(strategy))
}
// SwapExtended allows you to specify how the response will be swapped in relative to the target of an AJAX request, with modifiers for changing the behavior of the swap.
@@ -417,8 +387,8 @@ func (hx *HX) Swap(strategy swap.Strategy) *HX {
// HTMX Attribute: [hx-swap]
//
// [hx-swap]: https://htmx.org/attributes/hx-swap
-func (hx *HX) SwapExtended(swap *swap.Builder) *HX {
- return hx.set(Swap, swap.String())
+func (hx *HX[T]) SwapExtended(swap *swap.Builder) T {
+ return hx.attr(Swap, swap.String())
}
// SwapOOP allows you to specify that some content in a response should be swapped into the DOM somewhere other than the target by ID, that is “Out of Band”. This allows you to piggy back updates to other element updates on a response.
@@ -443,8 +413,8 @@ func (hx *HX) SwapExtended(swap *swap.Builder) *HX {
// HTMX Attribute: [hx-swap-oob]
//
// [hx-swap-oob]: https://htmx.org/attributes/hx-swap-oob
-func (hx *HX) SwapOOB() *HX {
- return hx.set(SwapOOB, "true")
+func (hx *HX[T]) SwapOOB() T {
+ return hx.attr(SwapOOB, "true")
}
// SwapOOBWithStrategy allows you to specify that some content in a response should be swapped into the DOM somewhere other than the target by ID with a swap strategy, that is “Out of Band”. This allows you to piggy back updates to other element updates on a response.
@@ -469,8 +439,8 @@ func (hx *HX) SwapOOB() *HX {
// HTMX Attribute: [hx-swap-oob]
//
// [hx-swap-oob]: https://htmx.org/attributes/hx-swap-oob
-func (hx *HX) SwapOOBWithStrategy(strategy swap.Strategy) *HX {
- return hx.set(SwapOOB, string(strategy))
+func (hx *HX[T]) SwapOOBWithStrategy(strategy swap.Strategy) T {
+ return hx.attr(SwapOOB, string(strategy))
}
// SwapOOP allows you to specify that some content in a response should be swapped into the DOM somewhere other than the target by selector, that is “Out of Band”. This allows you to piggy back updates to other element updates on a response.
@@ -495,8 +465,8 @@ func (hx *HX) SwapOOBWithStrategy(strategy swap.Strategy) *HX {
// HTMX Attribute: [hx-swap-oob]
//
// [hx-swap-oob]: https://htmx.org/attributes/hx-swap-oob
-func (hx *HX) SwapOOBSelector(strategy swap.Strategy, extendedSelector string) *HX {
- return hx.set(SwapOOB, fmt.Sprintf("%s:%s", strategy, extendedSelector))
+func (hx *HX[T]) SwapOOBSelector(strategy swap.Strategy, extendedSelector string) T {
+ return hx.attr(SwapOOB, fmt.Sprintf("%s:%s", strategy, extendedSelector))
}
type TargetSelector string
@@ -537,8 +507,8 @@ var TargetRelative = makeRelativeSelector[SelectorModifier, TargetSelector]()
// HTMX Attribute: [hx-target]
//
// [hx-target]: https://htmx.org/attributes/hx-target
-func (hx *HX) Target(extendedSelector TargetSelector) *HX {
- return hx.set(Target, string(extendedSelector))
+func (hx *HX[T]) Target(extendedSelector TargetSelector) T {
+ return hx.attr(Target, string(extendedSelector))
}
// Trigger allows you to specify what event triggers an AJAX request.
@@ -548,8 +518,8 @@ func (hx *HX) Target(extendedSelector TargetSelector) *HX {
// HTMX Attribute: [hx-trigger]
//
// [hx-trigger]: https://htmx.org/attributes/hx-trigger/
-func (hx *HX) Trigger(event string) *HX {
- return hx.set(Trigger, event)
+func (hx *HX[T]) Trigger(event string) T {
+ return hx.attr(Trigger, event)
}
// TriggerExtended allows you to specify what triggers an AJAX request, with modifiers for changing the behavior of the trigger.
@@ -564,13 +534,13 @@ func (hx *HX) Trigger(event string) *HX {
// HTMX Attribute: [hx-trigger]
//
// [hx-trigger]: https://htmx.org/attributes/hx-trigger/
-func (hx *HX) TriggerExtended(triggers ...trigger.Trigger) *HX {
+func (hx *HX[T]) TriggerExtended(triggers ...trigger.Trigger) T {
values := make([]string, len(triggers))
for i, t := range triggers {
values[i] = t.String()
}
- return hx.set(Trigger, strings.Join(values, ", "))
+ return hx.attr(Trigger, strings.Join(values, ", "))
}
// Vals allows you to add to the parameters that will be submitted with an AJAX request.
@@ -588,13 +558,14 @@ func (hx *HX) TriggerExtended(triggers ...trigger.Trigger) *HX {
// HTMX Attribute: [hx-vals]
//
// [hx-vals]: https://htmx.org/attributes/hx-vals
-func (hx *HX) Vals(vals any) *HX {
+func (hx *HX[T]) Vals(vals any) T {
json, err := json.Marshal(vals)
if err != nil {
// Silently ignore the value if there is an error, because there's not a good way to report an error when constructing templ attributes.
- return hx
+ var empty T
+ return empty
}
- return hx.set(Vals, string(json))
+ return hx.attr(Vals, string(json))
}
// ValsJS allows you to add to the parameters that will be submitted with an AJAX request, using JavaScript to compute the values.
@@ -620,8 +591,8 @@ func (hx *HX) Vals(vals any) *HX {
// HTMX Attribute: [hx-vals]
//
// [hx-vals]: https://htmx.org/attributes/hx-val
-func (hx *HX) ValsJS(vals map[string]string) *HX {
- return hx.set(Vals, mapToJS(vals))
+func (hx *HX[T]) ValsJS(vals map[string]string) T {
+ return hx.attr(Vals, mapToJS(vals))
}
// Additional Attributes
@@ -651,8 +622,8 @@ func (hx *HX) ValsJS(vals map[string]string) *HX {
// HTMX Attribute: [hx-confirm]
//
// [hx-confirm]: https://htmx.org/attributes/hx-confirm/
-func (hx *HX) Confirm(msg string) *HX {
- return hx.set(Confirm, msg)
+func (hx *HX[T]) Confirm(msg string) T {
+ return hx.attr(Confirm, msg)
}
// Delete will cause an element to issue a DELETE to the specified URL and swap the HTML into the DOM using a swap strategy:
@@ -677,8 +648,8 @@ func (hx *HX) Confirm(msg string) *HX {
// [hx-delete]: https://htmx.org/attributes/hx-delete
// [Parameters]: https://htmx.org/docs/#parameters
// [Requests & Responses]: https://htmx.org/docs/#requests
-func (hx *HX) Delete(url string) *HX {
- return hx.set(Delete, url)
+func (hx *HX[T]) Delete(url string) T {
+ return hx.attr(Delete, url)
}
// Disable will disable htmx processing for a given element and all its children. This can be useful as a backup for HTML escaping, when you include user generated content in your site, and you want to prevent malicious scripting attacks.
@@ -688,8 +659,8 @@ func (hx *HX) Delete(url string) *HX {
// HTMX Attribute: [hx-disable]
//
// [hx-disable]: https://htmx.org/attributes/hx-disable
-func (hx *HX) Disable() *HX {
- return hx.set(Disable, true)
+func (hx *HX[T]) Disable() T {
+ return hx.attr(Disable, true)
}
type DisabledEltModifier string
@@ -717,8 +688,8 @@ var DisabledEltRelative = makeRelativeSelector[DisabledEltModifier, DisabledEltS
// HTMX Attribute: [hx-disabled-elt]
//
// [hx-disabled-elt]: https://htmx.org/attributes/hx-disabled-elt
-func (hx *HX) DisabledElt(selector DisabledEltSelector) *HX {
- return hx.set(DisabledElt, string(selector))
+func (hx *HX[T]) DisabledElt(selector DisabledEltSelector) T {
+ return hx.attr(DisabledElt, string(selector))
}
// Disinherit allows you to disable automatic attribute inheritance for one or multiple specified attributes.
@@ -749,14 +720,14 @@ func (hx *HX) DisabledElt(selector DisabledEltSelector) *HX {
//
// [hx-disinherit]: https://htmx.org/attributes/hx-disinherit/
// [Attribute Inheritance]: https://htmx.org/docs/#inheritance
-func (hx *HX) Disinherit(attr ...Attribute) *HX {
+func (hx *HX[T]) Disinherit(attr ...Attribute) T {
// Convert to strings for joining.
attrStrings := make([]string, len(attr))
for i, a := range attr {
attrStrings[i] = string(a)
}
- return hx.set(Disinherit, strings.Join(attrStrings, " "))
+ return hx.attr(Disinherit, strings.Join(attrStrings, " "))
}
// DisinheritAll allows you to disable automatic attribute inheritance for all attributes.
@@ -779,16 +750,15 @@ func (hx *HX) Disinherit(attr ...Attribute) *HX {
//
// [hx-disinherit]: https://htmx.org/attributes/hx-disinherit/
// [Attribute Inheritance]: https://htmx.org/docs/#inheritance
-func (hx *HX) DisinheritAll() *HX {
- return hx.set(Disinherit, "*")
+func (hx *HX[T]) DisinheritAll() T {
+ return hx.attr(Disinherit, "*")
}
// An EncodingContentType is a valid encoding override for an [HX.Encoding()].
type EncodingContentType string
-const (
- EncodingMultipart EncodingContentType = "multipart/form-data" // support file uploads in an ajax request
-)
+// support file uploads in an ajax request
+const EncodingMultipart EncodingContentType = "multipart/form-data"
// Encoding allows you to switch the request encoding from the usual application/x-www-form-urlencoded encoding to multipart/form-data, usually to support file uploads in an ajax request.
//
@@ -803,8 +773,8 @@ const (
// HTMX Attribute: [hx-encoding]
//
// [hx-encoding]: https://htmx.org/attributes/hx-encoding
-func (hx *HX) Encoding(encoding EncodingContentType) *HX {
- return hx.set(Encoding, string(encoding))
+func (hx *HX[T]) Encoding(encoding EncodingContentType) T {
+ return hx.attr(Encoding, string(encoding))
}
// Ext enables an htmx [extension] for an element and all its children.
@@ -821,8 +791,8 @@ func (hx *HX) Encoding(encoding EncodingContentType) *HX {
//
// [hx-ext]: https://htmx.org/attributes/hx-ext
// [extension]: https://htmx.org/extensions
-func (hx *HX) Ext(ext ...string) *HX {
- return hx.set(Ext, strings.Join(ext, ","))
+func (hx *HX[T]) Ext(ext ...string) T {
+ return hx.attr(Ext, strings.Join(ext, ","))
}
// ExtIgnore ignores an [extension] that is defined by a parent node.
@@ -838,8 +808,8 @@ func (hx *HX) Ext(ext ...string) *HX {
//
// [hx-ext]: https://htmx.org/attributes/hx-ext
// [extension]: https://htmx.org/extensions
-func (hx *HX) ExtIgnore(ext string) *HX {
- return hx.set(Ext, fmt.Sprintf("ignore:%s", ext))
+func (hx *HX[T]) ExtIgnore(ext string) T {
+ return hx.attr(Ext, fmt.Sprintf("ignore:%s", ext))
}
// Headers allows you to add to the headers that will be submitted with an AJAX request.
@@ -856,13 +826,14 @@ func (hx *HX) ExtIgnore(ext string) *HX {
// HTMX Attribute: [hx-headers]
//
// [hx-headers]: https://htmx.org/attributes/hx-headers
-func (hx *HX) Headers(headers any) *HX {
+func (hx *HX[T]) Headers(headers any) T {
json, err := json.Marshal(headers)
if err != nil {
// Silently ignore the value if there is an error, because there's not a good way to report an error when constructing templ attributes.
- return hx
+ var empty T
+ return empty
}
- return hx.set(Headers, string(json))
+ return hx.attr(Headers, string(json))
}
// HeadersJS allows you to add to the headers that will be submitted with an AJAX request, with values evaluated as JavaScript expressions at runtime.
@@ -881,8 +852,8 @@ func (hx *HX) Headers(headers any) *HX {
// HTMX Attribute: [hx-headers]
//
// [hx-headers]: https://htmx.org/attributes/hx-headers
-func (hx *HX) HeadersJS(headers map[string]string) *HX {
- return hx.set(Headers, mapToJS(headers))
+func (hx *HX[T]) HeadersJS(headers map[string]string) T {
+ return hx.attr(Headers, mapToJS(headers))
}
// History when set to false on any element in the current document, or any html fragment loaded into the current document by htmx, will prevent sensitive data being saved to the localStorage cache when htmx takes a snapshot of the page state.
@@ -905,8 +876,8 @@ func (hx *HX) HeadersJS(headers map[string]string) *HX {
// HTMX Attribute: [hx-history]
//
// [hx-history]: https://htmx.org/attributes/hx-history/
-func (hx *HX) History(on bool) *HX {
- return hx.set(History, boolToString(on))
+func (hx *HX[T]) History(on bool) T {
+ return hx.attr(History, boolToString(on))
}
// HistoryElt allows you to specify the element that will be used to snapshot and restore page state during navigation. By default, the body tag is used. This is typically good enough for most setups, but you may want to narrow it down to a child element. Just make sure that the element is always visible in your application, or htmx will not be able to restore history navigation properly.
@@ -929,8 +900,8 @@ func (hx *HX) History(on bool) *HX {
// HTMX Attribute: [hx-history-elt]
//
// [hx-history-elt]: https://htmx.org/attributes/hx-history-elt/
-func (hx *HX) HistoryElt() *HX {
- return hx.set(HistoryElt, true)
+func (hx *HX[T]) HistoryElt() T {
+ return hx.attr(HistoryElt, true)
}
type IncludeSelector string
@@ -944,8 +915,8 @@ var IncludeRelative = makeRelativeSelector[SelectorModifier, IncludeSelector]()
// HTMX Attribute: [hx-include]
//
// [hx-include]: https://htmx.org/attributes/hx-include/
-func (hx *HX) Include(selector IncludeSelector) *HX {
- return hx.set(Include, string(selector))
+func (hx *HX[T]) Include(selector IncludeSelector) T {
+ return hx.attr(Include, string(selector))
}
type IndicatorModifier string
@@ -963,8 +934,8 @@ var IndicatorRelative = makeRelativeSelector[IndicatorModifier, IndicatorSelecto
// HTMX Attribute: [hx-indicator]
//
// [hx-indicator]: https://htmx.org/attributes/hx-indicator/
-func (hx *HX) Indicator(extendedSelector IndicatorSelector) *HX {
- return hx.set(Indicator, string(extendedSelector))
+func (hx *HX[T]) Indicator(extendedSelector IndicatorSelector) T {
+ return hx.attr(Indicator, string(extendedSelector))
}
// ParamsAll allows you to include all parameters with an AJAX request (default).
@@ -980,8 +951,8 @@ func (hx *HX) Indicator(extendedSelector IndicatorSelector) *HX {
// HTMX Attribute: [hx-params]
//
// [hx-params]: https://htmx.org/attributes/hx-params/
-func (hx *HX) ParamsAll() *HX {
- return hx.set(Params, "*")
+func (hx *HX[T]) ParamsAll() T {
+ return hx.attr(Params, "*")
}
// ParamsNone allows you to include no parameters with an AJAX request.
@@ -993,8 +964,8 @@ func (hx *HX) ParamsAll() *HX {
// HTMX Attribute: [hx-params]
//
// [hx-params]: https://htmx.org/attributes/hx-params/
-func (hx *HX) ParamsNone() *HX {
- return hx.set(Params, "none")
+func (hx *HX[T]) ParamsNone() T {
+ return hx.attr(Params, "none")
}
// Params allows you to filter the parameters that will be submitted with an AJAX request.
@@ -1006,8 +977,8 @@ func (hx *HX) ParamsNone() *HX {
// HTMX Attribute: [hx-params]
//
// [hx-params]: https://htmx.org/attributes/hx-params/
-func (hx *HX) Params(paramNames ...string) *HX {
- return hx.set(Params, strings.Join(paramNames, ","))
+func (hx *HX[T]) Params(paramNames ...string) T {
+ return hx.attr(Params, strings.Join(paramNames, ","))
}
// ParamsNot allows you to include all params except the comma separated list of parameter
@@ -1020,12 +991,12 @@ func (hx *HX) Params(paramNames ...string) *HX {
// HTMX Attribute: [hx-params]
//
// [hx-params]: https://htmx.org/attributes/hx-params/
-func (hx *HX) ParamsNot(paramNames ...string) *HX {
- return hx.set(Params, fmt.Sprintf("not %s", strings.Join(paramNames, ",")))
+func (hx *HX[T]) ParamsNot(paramNames ...string) T {
+ return hx.attr(Params, fmt.Sprintf("not %s", strings.Join(paramNames, ",")))
}
-func (hx *HX) Patch(url string) *HX {
- return hx.set(Patch, url)
+func (hx *HX[T]) Patch(url string) T {
+ return hx.attr(Patch, url)
}
// Preserve allows you to keep an element unchanged during HTML replacement. Elements with hx-preserve set are preserved by id when htmx updates any ancestor element. You must set an unchanging id on elements for hx-preserve to work. The response requires an element with the same id, but its type and other attributes are ignored.
@@ -1040,16 +1011,16 @@ func (hx *HX) Patch(url string) *HX {
//
// [hx-preserve]: https://htmx.org/attributes/hx-preserve/
// [morphdom extension]: https://htmx.org/extensions/morphdom
-func (hx *HX) Preserve() *HX {
- return hx.set(Preserve, true)
+func (hx *HX[T]) Preserve() T {
+ return hx.attr(Preserve, true)
}
-func (hx *HX) Prompt(msg string) *HX {
- return hx.set(Prompt, msg)
+func (hx *HX[T]) Prompt(msg string) T {
+ return hx.attr(Prompt, msg)
}
-func (hx *HX) Put(url string) *HX {
- return hx.set(Put, url)
+func (hx *HX[T]) Put(url string) T {
+ return hx.attr(Put, url)
}
// ReplaceURL allows you to replace the current url of the browser location history.
@@ -1076,8 +1047,8 @@ func (hx *HX) Put(url string) *HX {
// HTMX Attribute: [hx-replace]
//
// [hx-replace]: https://htmx.org/attributes/hx-replace/
-func (hx *HX) ReplaceURL(on bool) *HX {
- return hx.set(ReplaceURL, boolToString(on))
+func (hx *HX[T]) ReplaceURL(on bool) T {
+ return hx.attr(ReplaceURL, boolToString(on))
}
// ReplaceURLWith allows you to replace the current url of the browser location history with
@@ -1099,8 +1070,8 @@ func (hx *HX) ReplaceURL(on bool) *HX {
//
// [hx-replace]: https://htmx.org/attributes/hx-replace/
// [history.replaceState()]: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
-func (hx *HX) ReplaceURLWith(url string) *HX {
- return hx.set(ReplaceURL, url)
+func (hx *HX[T]) ReplaceURLWith(url string) T {
+ return hx.attr(ReplaceURL, url)
}
// RequestConfig describes static hx-request attributes
@@ -1133,8 +1104,8 @@ func (r RequestConfig) String() string {
// HTMX Attribute: [hx-request]
//
// [hx-request]: https://htmx.org/attributes/hx-request/
-func (hx *HX) Request(request RequestConfig) *HX {
- return hx.set(Request, request.String())
+func (hx *HX[T]) Request(request RequestConfig) T {
+ return hx.attr(Request, request.String())
}
// RequestConfigJS describes runtime hx-request attributes
@@ -1168,8 +1139,8 @@ func (r RequestConfigJS) String() string {
// HTMX Attribute: [hx-request]
//
// [hx-request]: https://htmx.org/attributes/hx-request/
-func (hx *HX) RequestJS(request RequestConfigJS) *HX {
- return hx.set(Request, request.String())
+func (hx *HX[T]) RequestJS(request RequestConfigJS) T {
+ return hx.attr(Request, request.String())
}
type SyncStrategy string
@@ -1203,8 +1174,8 @@ var SyncRelative = makeRelativeSelector[SelectorModifier, SyncSelector]()
// HTMX Attribute: [hx-sync]
//
// [hx-sync]: https://htmx.org/attributes/hx-sync/
-func (hx *HX) Sync(extendedSelector SyncSelector) *HX {
- return hx.set(Sync, string(extendedSelector))
+func (hx *HX[T]) Sync(extendedSelector SyncSelector) T {
+ return hx.attr(Sync, string(extendedSelector))
}
// SyncStrategy allows you to synchronize AJAX requests between multiple elements.
@@ -1219,8 +1190,8 @@ func (hx *HX) Sync(extendedSelector SyncSelector) *HX {
// HTMX Attribute: [hx-sync]
//
// [hx-sync]: https://htmx.org/attributes/hx-sync/
-func (hx *HX) SyncStrategy(extendedSelector SyncSelector, strategy SyncStrategy) *HX {
- return hx.set(Sync, fmt.Sprintf("%s:%s", extendedSelector, strategy))
+func (hx *HX[T]) SyncStrategy(extendedSelector SyncSelector, strategy SyncStrategy) T {
+ return hx.attr(Sync, fmt.Sprintf("%s:%s", extendedSelector, strategy))
}
// Validate will cause an element to validate itself by way of the HTML5 Validation API before it submits a request.
@@ -1233,40 +1204,26 @@ func (hx *HX) SyncStrategy(extendedSelector SyncSelector, strategy SyncStrategy)
// HTMX Attribute: [hx-validate]
//
// [hx-validate]: https://htmx.org/attributes/hx-validate/
-func (hx *HX) Validate(validate bool) *HX {
- return hx.set(Validate, boolToString(validate))
+func (hx *HX[T]) Validate(validate bool) T {
+ return hx.attr(Validate, boolToString(validate))
}
// Non-standard attributes
-// More allow you to merge arbitrary maps into the final attributes.
-// This allows additional attributes to be passed down in a single map.
-func (hx *HX) More(more map[string]any) *HX {
- for k, v := range more {
- hx.attrs[k] = v
- }
- return hx
-}
-
// Unset sets the value of the selected attributes as "unset" to clear a property that would normally be inherited (e.g. hx-confirm).
-func (hx *HX) Unset(attrs ...Attribute) *HX {
- for _, attr := range attrs {
- hx.set(attr, "unset")
- }
- return hx
+func (hx *HX[T]) Unset(attr Attribute) T {
+ return hx.attr(attr, "unset")
}
-// set sets a valid attribute to a value.
-func (hx *HX) set(key Attribute, value any) *HX {
- hx.attrs[string(key)] = value
- return hx
-}
+// // set sets a valid attribute to a value.
+// func (hx *HX[T]) set(key Attribute, value any) T {
+// return T{string(key): value}
+// }
-// set sets a non-standard attribute to a value.
-func (hx *HX) setOther(key string, value any) *HX {
- hx.attrs[key] = value
- return hx
-}
+// // set sets a non-standard attribute to a value.
+// func (hx *HX[T]) setOther(key string, value any) T {
+// return T{key: value}
+// }
// An Attribute is a valid HTMX attribute name. Used for general type changes like `unset` and `disinherit`.
type Attribute string
@@ -1302,11 +1259,8 @@ const (
Put Attribute = "hx-put"
ReplaceURL Attribute = "hx-replace-url"
Request Attribute = "hx-request"
- Sse Attribute = "hx-sse"
Sync Attribute = "hx-sync"
Validate Attribute = "hx-validate"
- Vars Attribute = "hx-vars"
- WS Attribute = "hx-ws"
)
// A SelectorModifier is a relative modifier to a CSS selector. This is used for "extended selectors".
diff --git a/htmx/htmx_test.go b/htmx/htmx_test.go
new file mode 100644
index 0000000..adb966f
--- /dev/null
+++ b/htmx/htmx_test.go
@@ -0,0 +1,352 @@
+package htmx_test
+
+import (
+ "fmt"
+ "time"
+
+ base "github.com/will-wow/typed-htmx-go/htmx"
+ "github.com/will-wow/typed-htmx-go/htmx/swap"
+ "github.com/will-wow/typed-htmx-go/htmx/trigger"
+)
+
+type Attrs map[string]any
+
+func (a Attrs) String() string {
+ for k, v := range a {
+ switch v := v.(type) {
+ // For strings, print the key='value' pair.
+ case string:
+ return fmt.Sprintf(`%s='%v'`, k, v)
+ // For booleans, print just the key if true.
+ case bool:
+ if v {
+ return k
+ }
+ }
+ }
+
+ return ""
+}
+
+var hx = base.NewHX(func(key base.Attribute, value any) Attrs {
+ return Attrs{string(key): value}
+})
+
+type HX = base.HX[Attrs]
+
+func ExampleHX_Boost() {
+ fmt.Println(hx.Boost(true))
+ // Output: hx-boost='true'
+}
+
+func ExampleHX_Get() {
+ fmt.Println(hx.Get("/example"))
+ // Output: hx-get='/example'
+}
+
+func ExampleHX_Post() {
+ fmt.Println(hx.Post("/example"))
+ // Output: hx-post='/example'
+}
+func ExampleHX_On() {
+ fmt.Println(hx.On("click", `alert("clicked")`))
+ // Output: hx-on:click='alert("clicked")'
+}
+
+func ExampleHX_OnHTMX() {
+ fmt.Println(hx.OnHTMX("before-request", `alert("before")`))
+ // Output: hx-on::before-request='alert("before")'
+}
+
+func ExampleHX_PushURL() {
+ fmt.Println(hx.PushURL(true))
+ // Output: hx-push-url='true'
+}
+
+func ExampleHX_PushURLPath() {
+ fmt.Println(hx.PushURLPath("/example"))
+ // Output: hx-push-url='/example'
+}
+
+func ExampleHX_Select() {
+ fmt.Println(hx.Select("#example"))
+ // Output: hx-select='#example'
+}
+
+func ExampleHX_SelectOOB() {
+ fmt.Println(hx.SelectOOB("#info-details", "#other-details"))
+ // Output: hx-select-oob='#info-details,#other-details'
+}
+
+func ExampleHX_SelectOOBWithStrategy() {
+ fmt.Println(hx.SelectOOBWithStrategy(
+ base.SelectOOBStrategy{Selector: "#info-details", Strategy: swap.AfterBegin},
+ base.SelectOOBStrategy{Selector: "#other-details", Strategy: ""},
+ ))
+ // Output: hx-select-oob='#info-details:afterbegin,#other-details'
+}
+
+func ExampleHX_Swap() {
+ fmt.Println(hx.Swap(swap.OuterHTML))
+ // Output: hx-swap='outerHTML'
+}
+
+func ExampleHX_SwapExtended() {
+ fmt.Println(hx.SwapExtended(
+ swap.New().
+ Strategy(swap.OuterHTML).
+ Settle(time.Second).
+ ShowElement("#example", swap.Top),
+ ))
+ // Output: hx-swap='outerHTML settle:1s show:#example:top'
+}
+
+func ExampleHX_SwapOOB() {
+ fmt.Println(hx.SwapOOB())
+ // Output: hx-swap-oob='true'
+}
+
+func ExampleHX_SwapOOBWithStrategy() {
+ fmt.Println(hx.SwapOOBWithStrategy(swap.AfterBegin))
+ // Output: hx-swap-oob='afterbegin'
+}
+
+func ExampleHX_SwapOOBSelector() {
+ fmt.Println(hx.SwapOOBSelector(swap.AfterBegin, "#example"))
+ // Output: hx-swap-oob='afterbegin:#example'
+}
+
+func ExampleHX_Target() {
+ fmt.Println(hx.Target("#example"))
+ // Output: hx-target='#example'
+}
+
+func ExampleHX_Target_nonStandard() {
+ fmt.Println(hx.Target(base.TargetThis))
+ // Output: hx-target='this'
+}
+
+func ExampleHX_Target_relativeSelector() {
+ fmt.Println(hx.Target(
+ base.TargetRelative(base.Closest, "#example"),
+ ))
+ // Output: hx-target='closest #example'
+}
+
+func ExampleHX_Trigger() {
+ fmt.Println(hx.Trigger("click"))
+ // Output: hx-trigger='click'
+}
+
+func ExampleHX_TriggerExtended() {
+ fmt.Println(hx.TriggerExtended(
+ trigger.NewEvent("click").Filter("ctrlKey").Target("#element"),
+ trigger.NewPoll(time.Second),
+ trigger.NewIntersectEvent().Root("#element").Threshold(0.2),
+ ))
+ // Output: hx-trigger='click[ctrlKey] target:#element, every 1s, intersect root:#element threshold:0.2'
+}
+
+func ExampleHX_Vals() {
+ fmt.Println(hx.Vals(map[string]int{"one": 1, "two": 2}))
+ // Output: hx-vals='{"one":1,"two":2}'
+}
+
+func ExampleHX_ValsJS() {
+ fmt.Println(hx.ValsJS(map[string]string{"lastKey": "event.key"}))
+ // Output: hx-vals='js:{lastKey:event.key}'
+}
+
+func ExampleHX_ValsJS_withInvalidIdentifier() {
+ fmt.Println(hx.ValsJS(map[string]string{"last-key": "event.key"}))
+ // Output: hx-vals='js:{"last-key":event.key}'
+}
+
+func ExampleHX_Confirm() {
+ fmt.Println(hx.Confirm("Are you sure?"))
+ // Output: hx-confirm='Are you sure?'
+}
+
+func ExampleHX_Delete() {
+ fmt.Println(hx.Delete("/example"))
+ // Output: hx-delete='/example'
+}
+
+func ExampleHX_Disable() {
+ fmt.Println(hx.Disable())
+ // Output: hx-disable
+}
+
+func ExampleHX_DisabledElt() {
+ fmt.Println(hx.DisabledElt("#example"))
+ // Output: hx-disabled-elt='#example'
+}
+
+func ExampleHX_DisabledElt_relative() {
+ fmt.Println(hx.DisabledElt(
+ base.DisabledEltRelative(base.DisabledEltClosest, "#example"),
+ ))
+ // Output: hx-disabled-elt='closest #example'
+}
+
+func ExampleHX_DisabledElt_this() {
+ fmt.Println(hx.DisabledElt(base.DisabledEltThis))
+ // Output: hx-disabled-elt='this'
+}
+
+func ExampleHX_Disinherit() {
+ fmt.Println(hx.Disinherit(base.Get, base.Boost))
+ // Output: hx-disinherit='hx-get hx-boost'
+}
+
+func ExampleHX_DisinheritAll() {
+ fmt.Println(hx.DisinheritAll())
+ // Output: hx-disinherit='*'
+}
+
+func ExampleHX_Encoding() {
+ fmt.Println(hx.Encoding(base.EncodingMultipart))
+ // Output: hx-encoding='multipart/form-data'
+}
+
+func ExampleHX_Ext() {
+ fmt.Println(hx.Ext("example-extension"))
+ // Output: hx-ext='example-extension'
+}
+
+func ExampleHX_ExtIgnore() {
+ fmt.Println(hx.ExtIgnore("example-extension"))
+ // Output: hx-ext='ignore:example-extension'
+}
+
+func ExampleHX_Headers() {
+ fmt.Println(hx.Headers(map[string]string{"Content-Type": "application/json"}))
+ // Output: hx-headers='{"Content-Type":"application/json"}'
+}
+
+func ExampleHX_HeadersJS() {
+ fmt.Println(hx.HeadersJS(map[string]string{"Content-Type": "getContentType()"}))
+ // Output: hx-headers='js:{"Content-Type":getContentType()}'
+}
+
+func ExampleHX_History() {
+ fmt.Println(hx.History(true))
+ // Output: hx-history='true'
+}
+
+func ExampleHX_History_off() {
+ fmt.Println(hx.History(false))
+ // Output: hx-history='false'
+}
+
+func ExampleHX_HistoryElt() {
+ fmt.Println(hx.HistoryElt())
+ // Output: hx-history-elt
+}
+
+func ExampleHX_Include() {
+ fmt.Println(hx.Include("#example"))
+ // Output: hx-include='#example'
+}
+
+func ExampleHX_Include_this() {
+ fmt.Println(hx.Include(base.IncludeThis))
+ // Output: hx-include='this'
+}
+
+func ExampleHX_Include_relative() {
+ fmt.Println(hx.Include(
+ base.IncludeRelative(base.Closest, "#example"),
+ ))
+ // Output: hx-include='closest #example'
+}
+
+func ExampleHX_Indicator() {
+ fmt.Println(hx.Indicator("#example"))
+ // Output: hx-indicator='#example'
+}
+
+func ExampleHX_Indicator_relative() {
+ fmt.Println(hx.Indicator(
+ base.IndicatorRelative(base.IndicatorClosest, "#example"),
+ ))
+ // Output: hx-indicator='closest #example'
+}
+
+func ExampleHX_ParamsAll() {
+ fmt.Println(hx.ParamsAll())
+ // Output: hx-params='*'
+}
+
+func ExampleHX_ParamsNone() {
+ fmt.Println(hx.ParamsNone())
+ // Output: hx-params='none'
+}
+
+func ExampleHX_Params() {
+ fmt.Println(hx.Params("one", "two"))
+ // Output: hx-params='one,two'
+}
+
+func ExampleHX_ParamsNot() {
+ fmt.Println(hx.ParamsNot("one", "two"))
+ // Output: hx-params='not one,two'
+}
+
+func ExampleHX_Patch() {
+ fmt.Println(hx.Patch("/example"))
+ // Output: hx-patch='/example'
+}
+
+func ExampleHX_Preserve() {
+ fmt.Println(hx.Preserve())
+ // Output: hx-preserve
+}
+
+func ExampleHX_Prompt() {
+ fmt.Println(hx.Prompt("Enter a value"))
+ // Output: hx-prompt='Enter a value'
+}
+
+func ExampleHX_Put() {
+ fmt.Println(hx.Put("/example"))
+ // Output: hx-put='/example'
+}
+
+func ExampleHX_ReplaceURL() {
+ fmt.Println(hx.ReplaceURL(true))
+ // Output: hx-replace-url='true'
+}
+
+func ExampleHX_ReplaceURLWith() {
+ fmt.Println(hx.ReplaceURLWith("/example"))
+ // Output: hx-replace-url='/example'
+}
+
+func ExampleHX_Sync() {
+ fmt.Println(hx.Sync(base.SyncThis))
+ // Output: hx-sync='this'
+}
+
+func ExampleHX_SyncStrategy() {
+ fmt.Println(hx.SyncStrategy(base.SyncThis, base.SyncDrop))
+ // Output: hx-sync='this:drop'
+}
+
+func ExampleHX_SyncStrategy_relative() {
+ fmt.Println(hx.SyncStrategy(
+ base.SyncRelative(base.Closest, "#example"),
+ base.SyncDrop,
+ ))
+ // Output: hx-sync='closest #example:drop'
+}
+
+func ExampleHX_Validate() {
+ fmt.Println(hx.Validate(true))
+ // Output: hx-validate='true'
+}
+
+func ExampleHX_Unset() {
+ fmt.Println(hx.Unset(base.Boost))
+ // Output: hx-boost='unset'
+}
diff --git a/hx/internal/mod/mod.go b/htmx/internal/mod/mod.go
similarity index 100%
rename from hx/internal/mod/mod.go
rename to htmx/internal/mod/mod.go
diff --git a/hx/internal/mod/mod_test.go b/htmx/internal/mod/mod_test.go
similarity index 94%
rename from hx/internal/mod/mod_test.go
rename to htmx/internal/mod/mod_test.go
index b0997d3..4a610b5 100644
--- a/hx/internal/mod/mod_test.go
+++ b/htmx/internal/mod/mod_test.go
@@ -3,7 +3,7 @@ package mod_test
import (
"testing"
- "github.com/will-wow/typed-htmx-go/hx/internal/mod"
+ "github.com/will-wow/typed-htmx-go/htmx/internal/mod"
)
type StringAlias string
diff --git a/hx/swap/swap.go b/htmx/swap/swap.go
similarity index 99%
rename from hx/swap/swap.go
rename to htmx/swap/swap.go
index b13ff25..0decac9 100644
--- a/hx/swap/swap.go
+++ b/htmx/swap/swap.go
@@ -5,7 +5,7 @@ import (
"fmt"
"time"
- "github.com/will-wow/typed-htmx-go/hx/internal/mod"
+ "github.com/will-wow/typed-htmx-go/htmx/internal/mod"
)
// Modifier is an enum of the possible hx-swap modifiers.
diff --git a/hx/swap/swap_test.go b/htmx/swap/swap_test.go
similarity index 95%
rename from hx/swap/swap_test.go
rename to htmx/swap/swap_test.go
index c1f4ab3..229820c 100644
--- a/hx/swap/swap_test.go
+++ b/htmx/swap/swap_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"time"
- "github.com/will-wow/typed-htmx-go/hx/swap"
+ "github.com/will-wow/typed-htmx-go/htmx/swap"
)
func TestSwap(t *testing.T) {
diff --git a/htmx/templ.go b/htmx/templ.go
new file mode 100644
index 0000000..464a894
--- /dev/null
+++ b/htmx/templ.go
@@ -0,0 +1,25 @@
+package htmx
+
+import "github.com/a-h/templ"
+
+// NewTempl returns a HX instance for use with Templ.
+// Each attribute returns a templ.Attributes map, which can be spread into a templ element.
+func NewTempl() HX[templ.Attributes] {
+ return NewHX(
+ func(key Attribute, value any) templ.Attributes {
+ return templ.Attributes{string(key): value}
+ },
+ )
+}
+
+// TemplAttrs merges the given attributes into a single map.
+// This is helpful for passing many attributes to a templ component.
+func TemplAttrs(attrs ...templ.Attributes) templ.Attributes {
+ out := templ.Attributes{}
+ for _, a := range attrs {
+ for k, v := range a {
+ out[k] = v
+ }
+ }
+ return out
+}
diff --git a/htmx/templ_test.go b/htmx/templ_test.go
new file mode 100644
index 0000000..5b8f532
--- /dev/null
+++ b/htmx/templ_test.go
@@ -0,0 +1,17 @@
+package htmx_test
+
+import (
+ "testing"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
+)
+
+var templHx = htmx.NewTempl()
+
+func BenchmarkTempl(b *testing.B) {
+ attrs := make([]map[string]any, b.N)
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ attrs[i] = templHx.Boost(true)
+ }
+}
diff --git a/hx/trigger/event.go b/htmx/trigger/event.go
similarity index 99%
rename from hx/trigger/event.go
rename to htmx/trigger/event.go
index e7fa6ff..0a65414 100644
--- a/hx/trigger/event.go
+++ b/htmx/trigger/event.go
@@ -7,7 +7,7 @@ import (
"strings"
"time"
- "github.com/will-wow/typed-htmx-go/hx/internal/mod"
+ "github.com/will-wow/typed-htmx-go/htmx/internal/mod"
)
// Modifier is an enum of the possible hx-trigger modifiers.
diff --git a/hx/trigger/poll.go b/htmx/trigger/poll.go
similarity index 100%
rename from hx/trigger/poll.go
rename to htmx/trigger/poll.go
diff --git a/hx/trigger/trigger.go b/htmx/trigger/trigger.go
similarity index 100%
rename from hx/trigger/trigger.go
rename to htmx/trigger/trigger.go
diff --git a/hx/trigger/trigger_test.go b/htmx/trigger/trigger_test.go
similarity index 98%
rename from hx/trigger/trigger_test.go
rename to htmx/trigger/trigger_test.go
index 1522403..d45f5fc 100644
--- a/hx/trigger/trigger_test.go
+++ b/htmx/trigger/trigger_test.go
@@ -4,7 +4,7 @@ import (
"testing"
"time"
- "github.com/will-wow/typed-htmx-go/hx/trigger"
+ "github.com/will-wow/typed-htmx-go/htmx/trigger"
)
func TestNewEvent(t *testing.T) {
diff --git a/hx/hx_test.go b/hx/hx_test.go
deleted file mode 100644
index 712b4f9..0000000
--- a/hx/hx_test.go
+++ /dev/null
@@ -1,354 +0,0 @@
-package hx_test
-
-import (
- "testing"
- "time"
-
- "github.com/will-wow/typed-htmx-go/hx"
- "github.com/will-wow/typed-htmx-go/hx/swap"
- "github.com/will-wow/typed-htmx-go/hx/trigger"
-)
-
-func TestHX(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- want string
- attrs *hx.HX
- }{
- {
- name: "Boost",
- attrs: hx.New().Boost(true),
- want: `hx-boost='true'`,
- },
- {
- name: "Get",
- attrs: hx.New().Get("/example"),
- want: `hx-get='/example'`,
- },
- {
- name: "Post",
- attrs: hx.New().Post("/example"),
- want: `hx-post='/example'`,
- },
- {
- name: "On",
- attrs: hx.New().On("click", `alert("clicked")`),
- want: `hx-on:click='alert("clicked")'`,
- },
- {
- name: "OnHTMX",
- attrs: hx.New().OnHTMX("before-request", `alert("before")`),
- want: `hx-on::before-request='alert("before")'`,
- },
- {
- name: "PushURL",
- attrs: hx.New().PushURL(true),
- want: `hx-push-url='true'`,
- },
- {
- name: "PushURLPath",
- attrs: hx.New().PushURLPath("/example"),
- want: `hx-push-url='/example'`,
- },
- {
- name: "Select",
- attrs: hx.New().Select("#example"),
- want: `hx-select='#example'`,
- },
- {
- name: "SelectOOB",
- attrs: hx.New().SelectOOB("#info-details", "#other-details"),
- want: `hx-select-oob='#info-details,#other-details'`,
- },
- {
- name: "SelectOOBWithStrategy",
- attrs: hx.New().SelectOOBWithStrategy(
- hx.SelectOOBStrategy{Selector: "#info-details", Strategy: swap.AfterBegin},
- hx.SelectOOBStrategy{Selector: "#other-details", Strategy: ""},
- ),
- want: `hx-select-oob='#info-details:afterbegin,#other-details'`,
- },
- {
- name: "Swap",
- attrs: hx.New().Swap(swap.OuterHTML),
- want: `hx-swap='outerHTML'`,
- },
- {
- name: "SwapExtended",
- attrs: hx.New().SwapExtended(
- swap.New().
- Strategy(swap.OuterHTML).
- Settle(time.Second).
- ShowElement("#example", swap.Top),
- ),
- want: `hx-swap='outerHTML settle:1s show:#example:top'`,
- },
- {
- name: "SwapOOB",
- attrs: hx.New().SwapOOB(),
- want: `hx-swap-oob='true'`,
- },
- {
- name: "SwapOOBWithStrategy",
- attrs: hx.New().SwapOOBWithStrategy(swap.AfterBegin),
- want: `hx-swap-oob='afterbegin'`,
- },
- {
- name: "SwapOOBSelector",
- attrs: hx.New().SwapOOBSelector(swap.AfterBegin, "#example"),
- want: `hx-swap-oob='afterbegin:#example'`,
- },
- {
- name: "Target",
- attrs: hx.New().Target("#example"),
- want: `hx-target='#example'`,
- },
- {
- name: "Target non-standard",
- attrs: hx.New().Target(hx.TargetThis),
- want: `hx-target='this'`,
- },
- {
- name: "TargetSelector",
- attrs: hx.New().Target(
- hx.TargetRelative(hx.Closest, "#example"),
- ),
- want: `hx-target='closest #example'`,
- },
- {
- name: "Trigger",
- attrs: hx.New().Trigger("click"),
- want: `hx-trigger='click'`,
- },
- {
- name: "TriggerExtended",
- attrs: hx.New().TriggerExtended(
- trigger.NewEvent("click").Filter("ctrlKey").Target("#element"),
- trigger.NewPoll(time.Second),
- trigger.NewIntersectEvent().Root("#element").Threshold(0.2),
- ),
- want: `hx-trigger='click[ctrlKey] target:#element, every 1s, intersect root:#element threshold:0.2'`,
- },
- {
- name: "Vals",
- attrs: hx.New().Vals(map[string]int{"one": 1, "two": 2}),
- want: `hx-vals='{"one":1,"two":2}'`,
- },
- {
- name: "ValsJS",
- attrs: hx.New().ValsJS(map[string]string{"lastKey": "event.key"}),
- want: `hx-vals='js:{lastKey:event.key}'`,
- },
- {
- name: "ValsJS with invalid identifier",
- attrs: hx.New().ValsJS(map[string]string{"last-key": "event.key"}),
- want: `hx-vals='js:{"last-key":event.key}'`,
- },
- // Additional Attributes
- {
- name: "Confirm",
- attrs: hx.New().Confirm("Are you sure?"),
- want: `hx-confirm='Are you sure?'`,
- },
- {
- name: "Delete",
- attrs: hx.New().Delete("/example"),
- want: `hx-delete='/example'`,
- },
- {
- name: "Disable",
- attrs: hx.New().Disable(),
- want: `hx-disable`,
- },
- {
- name: "DisabledElt",
- attrs: hx.New().DisabledElt("#example"),
- want: `hx-disabled-elt='#example'`,
- },
- {
- name: "DisabledElt closest",
- attrs: hx.New().DisabledElt(
- hx.DisabledEltRelative(hx.DisabledEltClosest, "#example"),
- ),
- want: `hx-disabled-elt='closest #example'`,
- },
- {
- name: "DisabledElt this",
- attrs: hx.New().DisabledElt(hx.DisabledEltThis),
- want: `hx-disabled-elt='this'`,
- },
- {
- name: "Disinherit",
- attrs: hx.New().Disinherit(hx.Get, hx.Boost),
- want: `hx-disinherit='hx-get hx-boost'`,
- },
- {
- name: "DisinheritAll",
- attrs: hx.New().DisinheritAll(),
- want: `hx-disinherit='*'`,
- },
- {
- name: "Encoding",
- attrs: hx.New().Encoding(hx.EncodingMultipart),
- want: `hx-encoding='multipart/form-data'`,
- },
- {
- name: "Ext",
- attrs: hx.New().Ext("example-extension"),
- want: `hx-ext='example-extension'`,
- },
- {
- name: "ExtIgnore",
- attrs: hx.New().ExtIgnore("example-extension"),
- want: `hx-ext='ignore:example-extension'`,
- },
- {
- name: "Headers",
- attrs: hx.New().Headers(map[string]string{"Content-Type": "application/json"}),
- want: `hx-headers='{"Content-Type":"application/json"}'`,
- },
- {
- name: "HeadersJS",
- attrs: hx.New().HeadersJS(map[string]string{"Content-Type": "getContentType()"}),
- want: `hx-headers='js:{"Content-Type":getContentType()}'`,
- },
- {
- name: "History",
- attrs: hx.New().History(true),
- want: `hx-history='true'`,
- },
- {
- name: "History off",
- attrs: hx.New().History(false),
- want: `hx-history='false'`,
- },
- {
- name: "HistoryElt",
- attrs: hx.New().HistoryElt(),
- want: `hx-history-elt`,
- },
- {
- name: "Include",
- attrs: hx.New().Include("#example"),
- want: `hx-include='#example'`,
- },
- {
- name: "Include this",
- attrs: hx.New().Include(hx.IncludeThis),
- want: `hx-include='this'`,
- },
- {
- name: "Include relative",
- attrs: hx.New().Include(
- hx.IncludeRelative(hx.Closest, "#example"),
- ),
- want: `hx-include='closest #example'`,
- },
- {
- name: "Indicator",
- attrs: hx.New().Indicator("#example"),
- want: `hx-indicator='#example'`,
- },
- {
- name: "Indicator relative",
- attrs: hx.New().Indicator(
- hx.IndicatorRelative(hx.IndicatorClosest, "#example"),
- ),
- want: `hx-indicator='closest #example'`,
- },
- {
- name: "ParamsAll",
- attrs: hx.New().ParamsAll(),
- want: `hx-params='*'`,
- },
- {
- name: "ParamsNone",
- attrs: hx.New().ParamsNone(),
- want: `hx-params='none'`,
- },
- {
- name: "Params",
- attrs: hx.New().Params("one", "two"),
- want: `hx-params='one,two'`,
- },
- {
- name: "ParamsNot",
- attrs: hx.New().ParamsNot("one", "two"),
- want: `hx-params='not one,two'`,
- },
- {
- name: "Patch",
- attrs: hx.New().Patch("/example"),
- want: `hx-patch='/example'`,
- },
- {
- name: "Preserve",
- attrs: hx.New().Preserve(),
- want: `hx-preserve`,
- },
- {
- name: "Prompt",
- attrs: hx.New().Prompt("Enter a value"),
- want: `hx-prompt='Enter a value'`,
- },
- {
- name: "Put",
- attrs: hx.New().Put("/example"),
- want: `hx-put='/example'`,
- },
- {
- name: "ReplaceURL",
- attrs: hx.New().ReplaceURL(true),
- want: `hx-replace-url='true'`,
- },
- {
- name: "ReplaceURLWith",
- attrs: hx.New().ReplaceURLWith("/example"),
- want: `hx-replace-url='/example'`,
- },
- {
- name: "Sync",
- attrs: hx.New().Sync(hx.SyncThis),
- want: `hx-sync='this'`,
- },
- {
- name: "SyncStrategy",
- attrs: hx.New().SyncStrategy(hx.SyncThis, hx.SyncDrop),
- want: `hx-sync='this:drop'`,
- },
- {
- name: "SyncStrategy relative",
- attrs: hx.New().SyncStrategy(
- hx.SyncRelative(hx.Closest, "#example"),
- hx.SyncDrop,
- ),
- want: `hx-sync='closest #example:drop'`,
- },
- {
- name: "Validate",
- attrs: hx.New().Validate(true),
- want: `hx-validate='true'`,
- },
- {
- name: "More",
- attrs: hx.New().More(map[string]any{"method": "GET", "action": "/page"}),
- want: `action='/page' method='GET'`,
- },
- {
- name: "Unset",
- attrs: hx.New().Unset(hx.Boost, hx.Get),
- want: `hx-boost='unset' hx-get='unset'`,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got := tt.attrs.String()
- if got != tt.want {
- t.Errorf("got: %s, want: %s", got, tt.want)
- }
- })
- }
-}