+ This progress bar is updated every 600 milliseconds, with the width style attribute and aria-valuenow attribute set to current progress value. Because there is an id on the progress bar div, htmx will smoothly transition between requests by settling the style attribute into its new value. This, when coupled with CSS transitions, makes the visual transition continuous rather than jumpy.
+
+
+ Finally, when the process is complete, a server returns a HX-Trigger: done header, which triggers an update of the UI to “Complete” state with a restart button added to the UI (we are using the class-tools extension in this example to add fade-in effect on the button):
+
This progress bar is updated every 600 milliseconds, with the width style attribute and aria-valuenow attribute set to current progress value. Because there is an id on the progress bar div, htmx will smoothly transition between requests by settling the style attribute into its new value. This, when coupled with CSS transitions, makes the visual transition continuous rather than jumpy.
Finally, when the process is complete, a server returns a HX-Trigger: done header, which triggers an update of the UI to “Complete” state with a restart button added to the UI (we are using the class-tools extension in this example to add fade-in effect on the button):
")
+ 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 progressWidth(percent int) templ.Attributes {
+ return templ.Attributes{
+ "style": fmt.Sprintf("width: %d%%", percent),
+ }
+}
+
+//ex:end:progress
diff --git a/examples/web/progressbar/progressbar.go b/examples/web/progressbar/progressbar.go
new file mode 100644
index 0000000..4444f71
--- /dev/null
+++ b/examples/web/progressbar/progressbar.go
@@ -0,0 +1,163 @@
+package progressbar
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "sync/atomic"
+ "time"
+
+ htmx "github.com/angelofallars/htmx-go"
+
+ "github.com/will-wow/typed-htmx-go/examples/web/progressbar/exgom"
+ "github.com/will-wow/typed-htmx-go/examples/web/progressbar/extempl"
+ "github.com/will-wow/typed-htmx-go/examples/web/progressbar/shared"
+)
+
+type example struct {
+ gom bool
+ jobs *jobs
+}
+
+func NewHandler(gom bool) http.Handler {
+ mux := http.NewServeMux()
+
+ ex := example{
+ gom: gom,
+ jobs: newJobs(),
+ }
+
+ mux.HandleFunc("GET /{$}", ex.demo)
+ mux.HandleFunc("POST /job/{$}", ex.start)
+ mux.HandleFunc("GET /job/{id}/progress/{$}", ex.progress)
+ mux.HandleFunc("GET /job/{id}/{$}", ex.job)
+
+ return mux
+}
+
+func (ex *example) demo(w http.ResponseWriter, r *http.Request) {
+ if ex.gom {
+ _ = exgom.Page().Render(w)
+ } else {
+ _ = extempl.Page().Render(r.Context(), w)
+ }
+}
+
+func (ex *example) start(w http.ResponseWriter, r *http.Request) {
+ id := ex.jobs.add()
+
+ if ex.gom {
+ _ = exgom.JobRunning(id, 0).Render(w)
+ } else {
+ _ = extempl.JobRunning(id, 0).Render(r.Context(), w)
+ }
+}
+
+func (ex *example) progress(w http.ResponseWriter, r *http.Request) {
+ idParam := r.PathValue("id")
+ if idParam == "" {
+ http.Error(w, "missing id", http.StatusBadRequest)
+ return
+ }
+
+ id, err := strconv.ParseInt(idParam, 10, 64)
+ if err != nil {
+ http.Error(w, "invalid id", http.StatusBadRequest)
+ return
+ }
+
+ progress, err := ex.jobs.getProgress(id)
+ if err != nil {
+ http.Error(w, "job not found", http.StatusNotFound)
+ return
+ }
+
+ res := htmx.NewResponse()
+
+ if progress >= 100 {
+ res = res.AddTrigger(htmx.Trigger(shared.TriggerDone))
+ }
+
+ if ex.gom {
+ res.MustWrite(w)
+ _ = exgom.ProgressBar(progress).Render(w)
+ } else {
+ res.MustRenderTempl(r.Context(), w, extempl.ProgressBar(progress))
+ }
+}
+
+func (ex *example) job(w http.ResponseWriter, r *http.Request) {
+ idParam := r.PathValue("id")
+ if idParam == "" {
+ http.Error(w, "missing id", http.StatusBadRequest)
+ return
+ }
+
+ id, err := strconv.ParseInt(idParam, 10, 64)
+ if err != nil {
+ http.Error(w, "invalid id", http.StatusBadRequest)
+ return
+ }
+
+ progress, err := ex.jobs.getProgress(id)
+ if err != nil {
+ http.Error(w, "job not found", http.StatusNotFound)
+ return
+ }
+
+ if ex.gom {
+ _ = exgom.Job(id, progress).Render(w)
+ } else {
+ _ = extempl.Job(id, progress).Render(r.Context(), w)
+ }
+}
+
+type job struct {
+ duration time.Duration
+ startTime time.Time
+}
+
+type jobs struct {
+ nextID atomic.Int64
+ active map[int64]job
+}
+
+func newJobs() *jobs {
+ return &jobs{
+ nextID: atomic.Int64{},
+ active: map[int64]job{},
+ }
+}
+
+func (j *jobs) getNextID() int64 {
+ return j.nextID.Add(1)
+}
+
+func (j *jobs) add() int64 {
+ id := j.getNextID()
+
+ startTime := time.Now()
+ duration := time.Second * 6
+ job := job{
+ startTime: startTime,
+ duration: duration,
+ }
+
+ j.active[id] = job
+ return id
+}
+
+func (j *jobs) getProgress(id int64) (int, error) {
+ job, ok := j.active[id]
+ if !ok {
+ return 0, fmt.Errorf("job not found")
+ }
+
+ timeElapsed := time.Since(job.startTime)
+
+ if timeElapsed >= job.duration {
+ return 100, nil
+ }
+
+ return int((float64(timeElapsed) / float64(job.duration)) * 100), nil
+}
diff --git a/examples/web/progressbar/shared/shared.go b/examples/web/progressbar/shared/shared.go
new file mode 100644
index 0000000..d254c86
--- /dev/null
+++ b/examples/web/progressbar/shared/shared.go
@@ -0,0 +1,3 @@
+package shared
+
+const TriggerDone = "done"
diff --git a/examples/web/static/ex.go b/examples/web/static/ex.go
new file mode 100644
index 0000000..bef66c5
--- /dev/null
+++ b/examples/web/static/ex.go
@@ -0,0 +1,12 @@
+package static
+
+import (
+ "embed"
+
+ "github.com/will-wow/typed-htmx-go/examples/web/exprint"
+)
+
+//go:embed main.css
+var fs embed.FS
+
+var ExCSS = exprint.New(fs, "/*", "*/")
diff --git a/examples/web/static/main.css b/examples/web/static/main.css
index b408211..6cfb82f 100644
--- a/examples/web/static/main.css
+++ b/examples/web/static/main.css
@@ -19,3 +19,44 @@
.active-search.htmx-indicator.htmx-request {
opacity: 1;
}
+
+/*ex:start:progress-bar-style*/
+.progress-bar-demo .progress {
+ height: 20px;
+ margin-bottom: 20px;
+ overflow: hidden;
+ background-color: #f5f5f5;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+.progress-bar-demo .progress-bar {
+ float: left;
+ width: 0%;
+ height: 100%;
+ font-size: 12px;
+ line-height: 20px;
+ color: #fff;
+ text-align: center;
+ background-color: #337ab7;
+ -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ -webkit-transition: width 0.6s ease;
+ -o-transition: width 0.6s ease;
+ transition: width 0.6s ease;
+}
+/*ex:end:progress-bar-style*/
+.progress-bar-demo #restart-btn {
+ opacity: 0;
+}
+.progress-bar-demo #restart-btn.show {
+ opacity: 1;
+ transition: opacity 100ms ease-in;
+}
+
+.class-tools-ex .color {
+ color: purple;
+}
+
+.class-tools-ex .bold {
+ font-weight: bold;
+}
diff --git a/examples/web/web.go b/examples/web/web.go
index 69a267a..77381e8 100644
--- a/examples/web/web.go
+++ b/examples/web/web.go
@@ -9,8 +9,10 @@ import (
"github.com/will-wow/typed-htmx-go/examples/web/activesearch"
"github.com/will-wow/typed-htmx-go/examples/web/bulkupdate"
+ "github.com/will-wow/typed-htmx-go/examples/web/classtools_ex"
"github.com/will-wow/typed-htmx-go/examples/web/clicktoedit"
"github.com/will-wow/typed-htmx-go/examples/web/examples"
+ "github.com/will-wow/typed-htmx-go/examples/web/progressbar"
)
//go:embed "static"
@@ -50,19 +52,21 @@ func (h *Handler) routes() http.Handler {
mux.HandleFunc("/{$}", templIndexRoutes.NewIndexHandler)
mux.HandleFunc("/examples/gomponents/{$}", gomIndexRoutes.NewIndexHandler)
- delegateExample(mux, "/examples/templ/click-to-edit", clicktoedit.NewHandler(false))
- delegateExample(mux, "/examples/templ/bulk-update", bulkupdate.NewHandler(false))
- delegateExample(mux, "/examples/templ/active-search", activesearch.NewHandler(false))
-
- delegateExample(mux, "/examples/gomponents/click-to-edit", clicktoedit.NewHandler(true))
- delegateExample(mux, "/examples/gomponents/bulk-update", bulkupdate.NewHandler(true))
- delegateExample(mux, "/examples/gomponents/active-search", activesearch.NewHandler(true))
+ delegateExample(mux, "click-to-edit", clicktoedit.NewHandler)
+ delegateExample(mux, "bulk-update", bulkupdate.NewHandler)
+ delegateExample(mux, "active-search", activesearch.NewHandler)
+ delegateExample(mux, "progress-bar", progressbar.NewHandler)
+ delegateExample(mux, "class-tools", classtools_ex.NewHandler)
return h.recoverPanic(h.logRequest(mux))
}
-func delegateExample(mux *http.ServeMux, path string, handler http.Handler) {
- mux.Handle(path+"/", http.StripPrefix(path, handler))
+func delegateExample(mux *http.ServeMux, path string, handler func(bool) http.Handler) {
+ prefix := fmt.Sprintf("/examples/templ/%s", path)
+ mux.Handle(prefix+"/", http.StripPrefix(prefix, handler(false)))
+
+ prefix = fmt.Sprintf("/examples/gomponents/%s", path)
+ mux.Handle(prefix+"/", http.StripPrefix(prefix, handler(true)))
}
func (h *Handler) logRequest(next http.Handler) http.Handler {
diff --git a/go.work.sum b/go.work.sum
index 92a77ac..b84abdd 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -32,7 +32,7 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -41,8 +41,10 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
diff --git a/htmx/ext/classtools/classtools.go b/htmx/ext/classtools/classtools.go
new file mode 100644
index 0000000..127a65c
--- /dev/null
+++ b/htmx/ext/classtools/classtools.go
@@ -0,0 +1,123 @@
+package classtools
+
+import (
+ "strings"
+ "time"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
+)
+
+// Extension allows you to specify CSS classes that will be swapped onto or off of the elements by using a classes or data-classes attribute. This functionality allows you to apply CSS Transitions to your HTML without resorting to javascript.
+//
+// # Install
+//
+//
+//
+// Extension: [class-tools]
+//
+// [class-tools]: https://htmx.org/extensions/class-tools/
+const Extension htmx.Extension = "class-tools"
+
+// An operation represents the type of class operation to perform after the specified delay.
+type operation string
+
+const (
+ operationAdd operation = "add"
+ operationRemove operation = "remove"
+ operationToggle operation = "toggle"
+)
+
+// A classOperation represents a single operation to be performed on a class, after a delay.
+type classOperation struct {
+ operation operation
+ class string
+ delay time.Duration
+}
+
+// A run represents a sequence of class operations to be performed on an element.
+// construct classOperations using the [Add], [Remove], and [Toggle] functions.
+type Run []classOperation
+
+// Classes allows you to specify CSS classes that will be swapped onto or off of the elements by using a classes or data-classes attribute. This functionality allows you to apply CSS Transitions to your HTML without resorting to javascript.
+// A classes attribute value consists one or more [Run]s of class operations. All class operations within a given Run will be applied sequentially, with the delay specified.
+// Use [ClassesParallel] to specify multiple parallel runs of class operations.
+// A class operation is an operation ([Add], [Remove], or [Toggle]), a CSS class name, and an optional time delay. If the delay is not specified, the default delay is 100ms.
+//
+// # Usage
+//
+//
+//
+//
+//
+//
+//
+//
+// Extension: [class-tools]
+//
+// [class-tools]: https://htmx.org/extensions/class-tools/
+func Classes[T any](hx htmx.HX[T], operations ...classOperation) T {
+ return ClassesParallel(hx, []Run{operations})
+}
+
+// ClassesParallel allows you to specify multiple runs of CSS classes that will be swapped onto or off of the elements by using a classes or data-classes attribute. This functionality allows you to apply CSS Transitions to your HTML without resorting to javascript.
+// A classes attribute value consists one or more [Run]s of class operations. All class operations within a given Run will be applied sequentially, with the delay specified.
+// Use [Classes] to more concisely specify a single runs of class operations.
+// A class operation is an operation ([Add], [Remove], or [Toggle]), a CSS class name, and a time delay (which can be 0).
+//
+// # Usage
+//
+//
+//
+//
+//
+// Extension: [class-tools]
+//
+// [class-tools]: https://htmx.org/extensions/class-tools/
+func ClassesParallel[T any](hx htmx.HX[T], runs []Run) T {
+ classes := strings.Builder{}
+
+ for i, run := range runs {
+ for j, op := range run {
+ classes.WriteString(string(op.operation))
+ classes.WriteRune(' ')
+ classes.WriteString(op.class)
+ classes.WriteRune(':')
+ classes.WriteString(op.delay.String())
+ if j < len(run)-1 {
+ classes.WriteString(", ")
+ }
+ }
+ if i < len(runs)-1 {
+ classes.WriteString(" & ")
+ }
+ }
+
+ return hx.Attr("classes", classes.String())
+}
+
+// Add will add a class to the element after the specified delay.
+// Only the first delay value will be used.
+func Add(className string, delay time.Duration) classOperation {
+ return makeOperation(operationAdd, className, delay)
+}
+
+// Remove will remove a class from the element after the specified delay.
+func Remove(className string, delay time.Duration) classOperation {
+ return makeOperation(operationRemove, className, delay)
+}
+
+// Toggle will toggle a class on the element on and off, every time the delay elapses.
+func Toggle(className string, delay time.Duration) classOperation {
+ return makeOperation(operationToggle, className, delay)
+}
+
+func makeOperation(op operation, className string, delay time.Duration) classOperation {
+ return classOperation{
+ operation: op,
+ class: className,
+ delay: delay,
+ }
+}
diff --git a/htmx/ext/classtools/classtools_test.go b/htmx/ext/classtools/classtools_test.go
new file mode 100644
index 0000000..0994bb6
--- /dev/null
+++ b/htmx/ext/classtools/classtools_test.go
@@ -0,0 +1,41 @@
+package classtools_test
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
+ "github.com/will-wow/typed-htmx-go/htmx/ext/classtools"
+)
+
+var hx = htmx.NewStringAttrs()
+
+func ExampleClasses() {
+ attr := classtools.Classes(hx,
+ // Add foo after 500ms
+ classtools.Add("foo", 500*time.Millisecond),
+ // Remove bar immediately after
+ classtools.Remove("bar", 0),
+ // Then, start toggling baz every second
+ classtools.Toggle("baz", time.Second),
+ )
+ fmt.Println(attr)
+ // Output: classes='add foo:500ms, remove bar:0s, toggle baz:1s'
+}
+
+func ExampleClassesParallel() {
+ attr := classtools.ClassesParallel(hx, []classtools.Run{
+ {
+ // Add foo after 500ms
+ classtools.Add("foo", 500*time.Millisecond),
+ // Remove bar immediately after
+ classtools.Remove("bar", 0),
+ },
+ {
+ // Also, toggle baz every second
+ classtools.Toggle("baz", time.Second),
+ },
+ })
+ fmt.Println(attr)
+ // Output: classes='add foo:500ms, remove bar:0s & toggle baz:1s'
+}
diff --git a/htmx/ext/preload/preload.go b/htmx/ext/preload/preload.go
new file mode 100644
index 0000000..e98afdd
--- /dev/null
+++ b/htmx/ext/preload/preload.go
@@ -0,0 +1,53 @@
+package preload
+
+import (
+ "github.com/will-wow/typed-htmx-go/htmx"
+ "github.com/will-wow/typed-htmx-go/htmx/internal/util"
+)
+
+// Extension allows you to load HTML fragments into your browser’s cache before they are requested by the user, so that additional pages appear to users to load nearly instantaneously. As a developer, you can customize its behavior to fit your applications needs and use cases.
+//
+// # Install
+//
+//
+//
+// Extension: [preload]
+//
+// [preload]: https://htmx.org/extensions/preload/
+const Extension htmx.Extension = "preload"
+
+// Preload adds a preload attribute to any hyperlinks and hx-get elements you want to preload. By default, resources will be loaded as soon as the mousedown event begins, giving your application a roughly 100-200ms head start on serving responses.
+//
+// Extension: [preload]
+//
+// [preload]: https://htmx.org/extensions/preload/
+func Preload[T any](hx htmx.HX[T]) T {
+ return hx.Attr("preload", true)
+}
+
+// A PreloadEvent represents the event that triggers the preload.
+type PreloadEvent string
+
+const (
+ MouseDown PreloadEvent = "mousedown" // The default behavior for this extension is to begin loading a resource when the user presses the mouse down.
+ MouseOver PreloadEvent = "mouseover" // To preload links more aggressively, you can trigger the preload to happen when the user’s mouse hovers over the link instead.
+ Init PreloadEvent = "preload:init" // The extension itself generates an event called preload:init that can be used to trigger preloads as soon as an object has been processed by htmx.
+)
+
+// PreloadOn adds a preload attribute to any hyperlinks and hx-get elements you want to preload, specifying the event that triggers the preload.
+//
+// Extension: [preload]
+//
+// [preload]: https://htmx.org/extensions/preload/#preload-mouseover
+func PreloadOn[T any](hx htmx.HX[T], event PreloadEvent) T {
+ return hx.Attr("preload", string(event))
+}
+
+// PreloadImages preloads linked image resources after an HTML page (or page fragment) is preloaded.
+//
+// Extension: [preload]
+//
+// [preload]: https://htmx.org/extensions/preload/#preloading-of-linked-images
+func PreloadImages[T any](hx htmx.HX[T], preloadImages bool) T {
+ return hx.Attr("preload-images", util.BoolToString(preloadImages))
+}
diff --git a/htmx/ext/preload/preload_test.go b/htmx/ext/preload/preload_test.go
new file mode 100644
index 0000000..8eb00ea
--- /dev/null
+++ b/htmx/ext/preload/preload_test.go
@@ -0,0 +1,34 @@
+package preload_test
+
+import (
+ "fmt"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
+ "github.com/will-wow/typed-htmx-go/htmx/ext/preload"
+)
+
+var hx = htmx.NewStringAttrs()
+
+func ExamplePreload() {
+ attr := preload.Preload(hx)
+ fmt.Println(attr)
+ // Output: preload
+}
+
+func ExamplePreloadOn() {
+ attr := preload.PreloadOn(hx, preload.MouseOver)
+ fmt.Println(attr)
+ // Output: preload='mouseover'
+}
+
+func ExamplePreloadOn_init() {
+ attr := preload.PreloadOn(hx, preload.Init)
+ fmt.Println(attr)
+ // Output: preload='preload:init'
+}
+
+func ExamplePreloadImages() {
+ attr := preload.PreloadImages(hx, true)
+ fmt.Println(attr)
+ // Output: preload-images='true'
+}
diff --git a/htmx/ext/removeme/removeme.go b/htmx/ext/removeme/removeme.go
new file mode 100644
index 0000000..4120cd4
--- /dev/null
+++ b/htmx/ext/removeme/removeme.go
@@ -0,0 +1,27 @@
+package removeme
+
+import (
+ "time"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
+)
+
+// Extension allows you to remove an element after a specified interval.
+//
+// # Install
+//
+//
+//
+// Extension: [remove-me]
+//
+// [remove-me]: https://htmx.org/extensions/remove-me/
+const Extension htmx.Extension = "remove-me"
+
+// RemoveMe removes the element after the specified interval.
+//
+// Extension: [remove-me]
+//
+// [remove-me]: https://htmx.org/extensions/remove-me/
+func RemoveMe[T any](hx htmx.HX[T], after time.Duration) T {
+ return hx.Attr("remove-me", after.String())
+}
diff --git a/htmx/ext/removeme/removeme_test.go b/htmx/ext/removeme/removeme_test.go
new file mode 100644
index 0000000..e379167
--- /dev/null
+++ b/htmx/ext/removeme/removeme_test.go
@@ -0,0 +1,17 @@
+package removeme_test
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/will-wow/typed-htmx-go/htmx"
+ "github.com/will-wow/typed-htmx-go/htmx/ext/removeme"
+)
+
+var hx = htmx.NewStringAttrs()
+
+func ExampleRemoveMe() {
+ attr := removeme.RemoveMe(hx, time.Second)
+ fmt.Println(attr)
+ // Output: remove-me='1s'
+}
diff --git a/htmx/htmx.go b/htmx/htmx.go
index 0cbcb1a..9e7f642 100644
--- a/htmx/htmx.go
+++ b/htmx/htmx.go
@@ -14,6 +14,7 @@ import (
"strings"
"time"
+ "github.com/will-wow/typed-htmx-go/htmx/internal/util"
"github.com/will-wow/typed-htmx-go/htmx/on"
"github.com/will-wow/typed-htmx-go/htmx/swap"
"github.com/will-wow/typed-htmx-go/htmx/trigger"
@@ -71,7 +72,7 @@ type StandardCSSSelector string
// [hx-boost]: https://htmx.org/attributes/hx-boost/
// [nice fallback]: https://en.wikipedia.org/wiki/Progressive_enhancement
func (hx *HX[T]) Boost(boost bool) T {
- return hx.attr("hx-boost", boolToString(boost))
+ return hx.attr("hx-boost", util.BoolToString(boost))
}
// Get will cause an element to issue a GET to the specified URL and swap the HTML into the DOM using a swap strategy.
@@ -92,7 +93,10 @@ func (hx *HX[T]) Boost(boost bool) T {
//
// [hx-get]: https://htmx.org/attributes/hx-get/
// [Parameters]: https://htmx.org/docs/#parameters
-func (hx *HX[T]) Get(url string) T {
+func (hx *HX[T]) Get(url string, a ...any) T {
+ if len(a) > 0 {
+ url = fmt.Sprintf(url, a...)
+ }
return hx.attr(Get, url)
}
@@ -115,7 +119,10 @@ func (hx *HX[T]) Get(url string) T {
//
// [hx-post]: https://htmx.org/attributes/hx-post/
// [Parameters]: https://htmx.org/docs/#parameters
-func (hx *HX[T]) Post(url string) T {
+func (hx *HX[T]) Post(url string, a ...any) T {
+ if len(a) > 0 {
+ url = fmt.Sprintf(url, a...)
+ }
return hx.attr(Post, url)
}
@@ -186,7 +193,7 @@ func (hx *HX[T]) On(event on.Event, action string) T {
//
// [hx-push-url]: https://htmx.org/attributes/hx-push-url/
func (hx *HX[T]) PushURL(on bool) T {
- return hx.attr(PushURL, boolToString(on))
+ return hx.attr(PushURL, util.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.
@@ -210,7 +217,11 @@ func (hx *HX[T]) PushURL(on bool) T {
// HTMX Attribute: [hx-push-url]
//
// [hx-push-url]: https://htmx.org/attributes/hx-push-url/
-func (hx *HX[T]) PushURLPath(url string) T {
+func (hx *HX[T]) PushURLPath(url string, a ...any) T {
+ if len(a) > 0 {
+ url = fmt.Sprintf(url, a...)
+ }
+
return hx.attr(PushURL, url)
}
@@ -284,7 +295,7 @@ func (hx *HX[T]) Select(selector StandardCSSSelector) T {
//
// [hx-select-oob]: https://htmx.org/attributes/hx-select-oob/
func (hx *HX[T]) SelectOOB(selectors ...StandardCSSSelector) T {
- return hx.attr(SelectOOB, joinStringLikes(selectors, ","))
+ return hx.attr(SelectOOB, util.JoinStringLikes(selectors, ","))
}
type SelectOOBStrategy struct {
@@ -463,7 +474,7 @@ const (
)
// TargetRelative allows you to narrow a CSS selector with an allowed relative modifier like `next`, and pass it to the [HX.Target()] attribute.
-var TargetRelative = makeRelativeSelector[RelativeModifier, TargetSelector]()
+var TargetRelative = util.MakeRelativeSelector[RelativeModifier, TargetSelector]()
// Target allows you to target a different element for swapping than the one issuing the AJAX request.
//
@@ -635,7 +646,11 @@ func (hx *HX[T]) Confirm(msg string) T {
// [hx-delete]: https://htmx.org/attributes/hx-delete
// [Parameters]: https://htmx.org/docs/#parameters
// [Requests & Responses]: https://htmx.org/docs/#requests
-func (hx *HX[T]) Delete(url string) T {
+func (hx *HX[T]) Delete(url string, a ...any) T {
+ if len(a) > 0 {
+ url = fmt.Sprintf(url, a...)
+ }
+
return hx.attr(Delete, url)
}
@@ -661,7 +676,7 @@ type DisabledEltSelector string
const DisabledEltThis DisabledEltSelector = "this" // indicates that this element should disable itself during the request.
// DisabledEltRelative allows you to narrow a CSS selector with the allowed relative modifier `closest`, and pass it to the [HX.DisabledElt] attribute.
-var DisabledEltRelative = makeRelativeSelector[DisabledEltModifier, DisabledEltSelector]()
+var DisabledEltRelative = util.MakeRelativeSelector[DisabledEltModifier, DisabledEltSelector]()
// DisabledElt allows you to specify elements that will have the disabled attribute added to them for the duration of the request.
//
@@ -781,6 +796,8 @@ func (hx *HX[T]) Encoding(encoding EncodingContentType) T {
return hx.attr(Encoding, string(encoding))
}
+type Extension string
+
// Ext enables an htmx [extension] for an element and all its children.
//
// The value can be one or more extension names to apply.
@@ -795,8 +812,12 @@ func (hx *HX[T]) Encoding(encoding EncodingContentType) T {
//
// [hx-ext]: https://htmx.org/attributes/hx-ext
// [extension]: https://htmx.org/extensions
-func (hx *HX[T]) Ext(ext ...string) T {
- return hx.attr(Ext, strings.Join(ext, ","))
+func (hx *HX[T]) Ext(ext ...Extension) T {
+ exts := make([]string, len(ext))
+ for i, e := range ext {
+ exts[i] = string(e)
+ }
+ return hx.attr(Ext, strings.Join(exts, ","))
}
// ExtIgnore ignores an [extension] that is defined by a parent node.
@@ -880,7 +901,7 @@ func (hx *HX[T]) HeadersJS(headers map[string]string) T {
//
// [hx-history]: https://htmx.org/attributes/hx-history/
func (hx *HX[T]) History(on bool) T {
- return hx.attr(History, boolToString(on))
+ return hx.attr(History, util.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.
@@ -912,7 +933,7 @@ type IncludeSelector string
const IncludeThis IncludeSelector = "this"
// IncludeRelative allows you to narrow a CSS selector with an allowed relative modifier like `next`, and pass it to the [HX.Include()] attribute.
-var IncludeRelative = makeRelativeSelector[RelativeModifier, IncludeSelector]()
+var IncludeRelative = util.MakeRelativeSelector[RelativeModifier, IncludeSelector]()
// Include allows you to include additional element values in an AJAX request.
//
@@ -930,7 +951,7 @@ const IndicatorClosest IndicatorModifier = "closest"
type IndicatorSelector string
// IndicatorRelative allows you to narrow a CSS selector with an allowed relative modifier like `next`, and pass it to the [HX.Indicator()] attribute.
-var IndicatorRelative = makeRelativeSelector[IndicatorModifier, IndicatorSelector]()
+var IndicatorRelative = util.MakeRelativeSelector[IndicatorModifier, IndicatorSelector]()
// The hx-indicator attribute allows you to specify the element that will have the htmx-request class added to it for the duration of the request. This can be used to show spinners or progress indicators while the request is in flight.
//
@@ -1020,7 +1041,11 @@ func (hx *HX[T]) ParamsNot(paramNames ...string) T {
//
// [Parameters]: https://htmx.org/docs/#parameters
// [hx-patch]: https://htmx.org/attributes/hx-patch/
-func (hx *HX[T]) Patch(url string) T {
+func (hx *HX[T]) Patch(url string, a ...any) T {
+ if len(a) > 0 {
+ url = fmt.Sprintf(url, a...)
+ }
+
return hx.attr(Patch, url)
}
@@ -1078,7 +1103,10 @@ func (hx *HX[T]) Prompt(msg string) T {
//
// [Parameters]: https://htmx.org/docs/#parameters
// [hx-put]: https://htmx.org/attributes/hx-put/
-func (hx *HX[T]) Put(url string) T {
+func (hx *HX[T]) Put(url string, a ...any) T {
+ if len(a) > 0 {
+ url = fmt.Sprintf(url, a...)
+ }
return hx.attr(Put, url)
}
@@ -1107,7 +1135,7 @@ func (hx *HX[T]) Put(url string) T {
//
// [hx-replace]: https://htmx.org/attributes/hx-replace/
func (hx *HX[T]) ReplaceURL(on bool) T {
- return hx.attr(ReplaceURL, boolToString(on))
+ return hx.attr(ReplaceURL, util.BoolToString(on))
}
// ReplaceURLWith allows you to replace the current url of the browser location history with
@@ -1129,7 +1157,11 @@ func (hx *HX[T]) ReplaceURL(on bool) T {
//
// [hx-replace]: https://htmx.org/attributes/hx-replace/
// [history.replaceState()]: https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState
-func (hx *HX[T]) ReplaceURLWith(url string) T {
+func (hx *HX[T]) ReplaceURLWith(url string, a ...any) T {
+ if len(a) > 0 {
+ url = fmt.Sprintf(url, a...)
+ }
+
return hx.attr(ReplaceURL, url)
}
@@ -1231,7 +1263,7 @@ type SyncSelector string
const SyncThis SyncSelector = "this" // synchronize requests from the current element.
// SyncRelative allows you to narrow a CSS selector with an allowed relative modifier like `next`, and pass it to the [HX.Sync()] attribute.
-var SyncRelative = makeRelativeSelector[RelativeModifier, SyncSelector]()
+var SyncRelative = util.MakeRelativeSelector[RelativeModifier, SyncSelector]()
// Sync allows you to synchronize AJAX requests between multiple elements, using a CSS selector to indicate the element to synchronize on.
//
@@ -1289,7 +1321,7 @@ func (hx *HX[T]) SyncStrategy(extendedSelector SyncSelector, strategy SyncStrate
//
// [hx-validate]: https://htmx.org/attributes/hx-validate/
func (hx *HX[T]) Validate(validate bool) T {
- return hx.attr(Validate, boolToString(validate))
+ return hx.attr(Validate, util.BoolToString(validate))
}
// Non-standard attributes
@@ -1299,6 +1331,15 @@ func (hx *HX[T]) Unset(attr Attribute) T {
return hx.attr(attr, "unset")
}
+type ExtAttribute struct {
+ Attribute string
+ Value any
+}
+
+func (hx *HX[T]) Attr(attribute Attribute, value any) T {
+ return hx.attr(attribute, value)
+}
+
// An Attribute is a valid HTMX attribute name. Used for general type changes like `unset` and `disinherit`.
type Attribute string
@@ -1348,13 +1389,6 @@ const (
Previous RelativeModifier = "previous" // scan the DOM backwards fo
)
-func boolToString(b bool) string {
- if b {
- return "true"
- }
- return "false"
-}
-
func mapToJS(vals map[string]string) string {
values := make([]string, len(vals))
@@ -1380,19 +1414,3 @@ func quoteJSIdentifier(identifier string) string {
}
return fmt.Sprintf(`"%s"`, identifier)
}
-
-// joinStringLikes joins a slice of string-like values into a single string.
-func joinStringLikes[T ~string](elems []T, sep string) string {
- var stringElems = make([]string, len(elems))
- for i, x := range elems {
- stringElems[i] = string(x)
- }
- return strings.Join(stringElems, sep)
-}
-
-// makeRelativeSelector creates a function that combines an allowed relative modifier with a CSS selector and returns a typed result.
-func makeRelativeSelector[Modifier ~string, Selector ~string]() func(Modifier, string) Selector {
- return func(modifier Modifier, selector string) Selector {
- return Selector(fmt.Sprintf("%s %s", modifier, selector))
- }
-}
diff --git a/htmx/htmx_test.go b/htmx/htmx_test.go
index 9fbe35f..ac67805 100644
--- a/htmx/htmx_test.go
+++ b/htmx/htmx_test.go
@@ -22,10 +22,21 @@ func ExampleHX_Get() {
// Output: hx-get='/example'
}
+func ExampleHX_Get_format() {
+ fmt.Println(hx.Get("/example/%d", 1))
+ // Output: hx-get='/example/1'
+}
+
func ExampleHX_Post() {
fmt.Println(hx.Post("/example"))
// Output: hx-post='/example'
}
+
+func ExampleHX_Post_format() {
+ fmt.Println(hx.Post("/example/%d", 1))
+ // Output: hx-post='/example/1'
+}
+
func ExampleHX_On() {
fmt.Println(hx.On("click", `alert("clicked")`))
// Output: hx-on:click='alert("clicked")'
@@ -46,6 +57,11 @@ func ExampleHX_PushURLPath() {
// Output: hx-push-url='/example'
}
+func ExampleHX_PushURLPath_format() {
+ fmt.Println(hx.PushURLPath("/example/%d", 1))
+ // Output: hx-push-url='/example/1'
+}
+
func ExampleHX_Select() {
fmt.Println(hx.Select("#example"))
// Output: hx-select='#example'
@@ -166,6 +182,11 @@ func ExampleHX_Delete() {
// Output: hx-delete='/example'
}
+func ExampleHX_Delete_format() {
+ fmt.Println(hx.Delete("/example/%d", 1))
+ // Output: hx-delete='/example/1'
+}
+
func ExampleHX_Disable() {
fmt.Println(hx.Disable())
// Output: hx-disable
@@ -297,6 +318,11 @@ func ExampleHX_Patch() {
// Output: hx-patch='/example'
}
+func ExampleHX_Patch_format() {
+ fmt.Println(hx.Patch("/example/%d", 1))
+ // Output: hx-patch='/example/1'
+}
+
func ExampleHX_Preserve() {
fmt.Println(hx.Preserve())
// Output: hx-preserve
@@ -312,6 +338,11 @@ func ExampleHX_Put() {
// Output: hx-put='/example'
}
+func ExampleHX_Put_format() {
+ fmt.Println(hx.Put("/example/%d", 1))
+ // Output: hx-put='/example/1'
+}
+
func ExampleHX_ReplaceURL() {
fmt.Println(hx.ReplaceURL(true))
// Output: hx-replace-url='true'
@@ -322,6 +353,11 @@ func ExampleHX_ReplaceURLWith() {
// Output: hx-replace-url='/example'
}
+func ExampleHX_ReplaceURLWith_format() {
+ fmt.Println(hx.ReplaceURLWith("/example/%d", 1))
+ // Output: hx-replace-url='/example/1'
+}
+
func ExampleHX_Request() {
fmt.Println(hx.Request(htmx.RequestConfig{
Timeout: time.Second,
diff --git a/htmx/internal/util/util.go b/htmx/internal/util/util.go
new file mode 100644
index 0000000..b1eca91
--- /dev/null
+++ b/htmx/internal/util/util.go
@@ -0,0 +1,29 @@
+package util
+
+import (
+ "fmt"
+ "strings"
+)
+
+func BoolToString(b bool) string {
+ if b {
+ return "true"
+ }
+ return "false"
+}
+
+// joinStringLikes joins a slice of string-like values into a single string.
+func JoinStringLikes[T ~string](elems []T, sep string) string {
+ var stringElems = make([]string, len(elems))
+ for i, x := range elems {
+ stringElems[i] = string(x)
+ }
+ return strings.Join(stringElems, sep)
+}
+
+// makeRelativeSelector creates a function that combines an allowed relative modifier with a CSS selector and returns a typed result.
+func MakeRelativeSelector[Modifier ~string, Selector ~string]() func(Modifier, string) Selector {
+ return func(modifier Modifier, selector string) Selector {
+ return Selector(fmt.Sprintf("%s %s", modifier, selector))
+ }
+}
diff --git a/htmx/internal/util/util_test.go b/htmx/internal/util/util_test.go
new file mode 100644
index 0000000..2d5d627
--- /dev/null
+++ b/htmx/internal/util/util_test.go
@@ -0,0 +1,58 @@
+package util_test
+
+import (
+ "testing"
+
+ "github.com/will-wow/typed-htmx-go/htmx/internal/util"
+)
+
+func TestBoolToString(t *testing.T) {
+ tests := []struct {
+ input bool
+ want string
+ }{
+ {input: true, want: "true"},
+ {input: false, want: "false"},
+ }
+ for _, test := range tests {
+ got := util.BoolToString(test.input)
+ if got != test.want {
+ t.Errorf("BoolToString(%v) = %v, want %v", test.input, got, test.want)
+ }
+ }
+}
+
+func TestJoinStringLikes(t *testing.T) {
+ t.Run("strings", func(t *testing.T) {
+ want := "a,b,c"
+ got := util.JoinStringLikes([]string{"a", "b", "c"}, ",")
+
+ if got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ })
+
+ t.Run("string likes", func(t *testing.T) {
+ type TestString string
+ want := "a,b,c"
+ got := util.JoinStringLikes([]TestString{"a", "b", "c"}, ",")
+
+ if got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+ })
+}
+
+func TestMakeRelativeSelector(t *testing.T) {
+ type TestModifier string
+ var next TestModifier = "next"
+ type TestSelector string
+
+ selector := util.MakeRelativeSelector[TestModifier, TestSelector]()
+ got := selector(next, "div")
+ var want TestSelector = "next div"
+
+ if got != want {
+ t.Errorf(`got "%v", want "%v"`, got, want)
+ }
+}
diff --git a/htmx/swap/swap.go b/htmx/swap/swap.go
index 9782221..4df4e33 100644
--- a/htmx/swap/swap.go
+++ b/htmx/swap/swap.go
@@ -6,6 +6,7 @@ import (
"time"
"github.com/will-wow/typed-htmx-go/htmx/internal/mod"
+ "github.com/will-wow/typed-htmx-go/htmx/internal/util"
)
// Modifier is an enum of the possible hx-swap modifiers.
@@ -147,7 +148,7 @@ func (s *Builder) ShowNone() *Builder {
//
// Alternatively, if you want the page to automatically scroll to the focused element after each request you can change the htmx global configuration value htmx.config.defaultFocusScroll to true. Then disable it for specific requests using focus-scroll:false.
func (s *Builder) FocusScroll(value bool) *Builder {
- s.modifiers[FocusScroll] = boolToString(value)
+ s.modifiers[FocusScroll] = util.BoolToString(value)
return s
}
@@ -157,10 +158,3 @@ func (s *Builder) Clear(modifier Modifier) *Builder {
delete(s.modifiers, modifier)
return s
}
-
-func boolToString(b bool) string {
- if b {
- return "true"
- }
- return "false"
-}
diff --git a/htmx/trigger/poll.go b/htmx/trigger/poll.go
index ccaf39f..10886e9 100644
--- a/htmx/trigger/poll.go
+++ b/htmx/trigger/poll.go
@@ -10,7 +10,7 @@ type Poll struct {
filter string
}
-// Every creates a new polling trigger.
+// Every creates a new polling trigger for [htmx.HX.TriggerExtended].
func Every(timing time.Duration) *Poll {
return &Poll{
timing: timing,