+ This progress bar is updated every 600 milliseconds, with the “width” style attribute and aria-valuenow attributed 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 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 attributed 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 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 attributed 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.
+ 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 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):
+ 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 attributed 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 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):
Add then remove bold after 1 second, then toggle color every second
Add then remove bold after 1 second, while toggling color every second\t\t
Add with no delay\t\t
Toggle with 0 delay\t\t
")
+ 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/examples/exgom/examples.gom.go b/examples/web/examples/exgom/examples.gom.go
index 950a4a4..2e4ab3b 100644
--- a/examples/web/examples/exgom/examples.gom.go
+++ b/examples/web/examples/exgom/examples.gom.go
@@ -40,15 +40,20 @@ func Page() g.Node {
"Demonstrates bulk updating of multiple rows of data",
),
exampleRow(
- "/examples/gomponents/active-search",
+ "/examples/gomponents/active-search/",
"Active Search",
"Demonstrates the active search box pattern",
),
exampleRow(
- "/examples/gomponents/progress-bar",
+ "/examples/gomponents/progress-bar/",
"Progress Bar",
"Demonstrates a job-runner like progress bar",
),
+ exampleRow(
+ "/examples/gomponents/class-tools/",
+ "Class Tools",
+ "Demo of class-tools options",
+ ),
),
),
)
diff --git a/examples/web/examples/extempl/examples.templ b/examples/web/examples/extempl/examples.templ
index c5dafed..77eb722 100644
--- a/examples/web/examples/extempl/examples.templ
+++ b/examples/web/examples/extempl/examples.templ
@@ -52,6 +52,11 @@ templ Page() {
"Progress Bar",
"Demonstrates a job-runner like progress bar",
)
+ @exampleRow(
+ "/examples/templ/class-tools/",
+ "Class Tools",
+ "Demo of class-tools options",
+ )
}
diff --git a/examples/web/examples/extempl/examples_templ.go b/examples/web/examples/extempl/examples_templ.go
index 382e0b4..cfa2cd8 100644
--- a/examples/web/examples/extempl/examples_templ.go
+++ b/examples/web/examples/extempl/examples_templ.go
@@ -70,6 +70,14 @@ func Page() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
+ templ_7745c5c3_Err = exampleRow(
+ "/examples/templ/class-tools/",
+ "Class Tools",
+ "Demo of class-tools options",
+ ).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
@@ -119,7 +127,7 @@ func exampleRow(link, name, description string) templ.Component {
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/extempl/examples.templ`, Line: 63, Col: 41}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/extempl/examples.templ`, Line: 68, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -132,7 +140,7 @@ func exampleRow(link, name, description string) templ.Component {
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/extempl/examples.templ`, Line: 66, Col: 16}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/extempl/examples.templ`, Line: 71, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
diff --git a/examples/web/progressbar/exgom/progressbar.gom.go b/examples/web/progressbar/exgom/progressbar.gom.go
index fbf741a..ed7a129 100644
--- a/examples/web/progressbar/exgom/progressbar.gom.go
+++ b/examples/web/progressbar/exgom/progressbar.gom.go
@@ -132,9 +132,9 @@ func Job(jobID int64, progress int) g.Node {
Button(
ID("restart-btn"),
hx.Post("/examples/gomponents/progress-bar/job/"),
- classtools.Classes(hx, []classtools.Run{{
+ classtools.Classes(hx,
classtools.Add("show", time.Millisecond*600),
- }}),
+ ),
g.Text("Restart Job"),
),
)
diff --git a/examples/web/progressbar/extempl/progressbar.templ b/examples/web/progressbar/extempl/progressbar.templ
index 6f331b5..2ae7977 100644
--- a/examples/web/progressbar/extempl/progressbar.templ
+++ b/examples/web/progressbar/extempl/progressbar.templ
@@ -119,9 +119,7 @@ templ Job(jobID int64, progress int) {
Restart Job
diff --git a/examples/web/progressbar/extempl/progressbar_templ.go b/examples/web/progressbar/extempl/progressbar_templ.go
index ff965ac..b0c36a3 100644
--- a/examples/web/progressbar/extempl/progressbar_templ.go
+++ b/examples/web/progressbar/extempl/progressbar_templ.go
@@ -319,9 +319,7 @@ func Job(jobID int64, progress int) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, classtools.Classes(hx, []classtools.Run{{
- classtools.Add("show", time.Millisecond*600),
- }}))
+ templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, classtools.Classes(hx, classtools.Add("show", time.Millisecond*600)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -409,7 +407,7 @@ func ProgressBar(progress int) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(progress))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 150, Col: 40}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 148, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
diff --git a/examples/web/static/main.css b/examples/web/static/main.css
index 7ffa1fd..6cfb82f 100644
--- a/examples/web/static/main.css
+++ b/examples/web/static/main.css
@@ -52,3 +52,11 @@
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 65e4e94..77381e8 100644
--- a/examples/web/web.go
+++ b/examples/web/web.go
@@ -9,6 +9,7 @@ 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"
@@ -55,6 +56,7 @@ func (h *Handler) routes() http.Handler {
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))
}
diff --git a/htmx/ext/classtools/classtools.go b/htmx/ext/classtools/classtools.go
index feb26f7..370c0fb 100644
--- a/htmx/ext/classtools/classtools.go
+++ b/htmx/ext/classtools/classtools.go
@@ -7,8 +7,10 @@ import (
"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.
const Extension htmx.Extension = "class-tools"
+// An operation represents the type of class operation to perform after the specified delay.
type operation string
const (
@@ -17,26 +19,57 @@ const (
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
+ 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
-func Classes[T any](hx htmx.HX[T], runs []Run) T {
+// 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
+//
+//
+//
+//
+//
+//
+//
+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
+//
+//
+//
+//
+func ClassesParallel[T any](hx htmx.HX[T], runs []Run) T {
classes := strings.Builder{}
for i, run := range runs {
- for j, class := range run {
- classes.WriteString(string(class.Operation))
+ for j, op := range run {
+ classes.WriteString(string(op.operation))
classes.WriteRune(' ')
- classes.WriteString(class.Class)
- if class.Delay > 0 {
- classes.WriteRune(':')
- classes.WriteString(class.Delay.String())
- }
+ classes.WriteString(op.class)
+ classes.WriteRune(':')
+ classes.WriteString(op.delay.String())
if j < len(run)-1 {
classes.WriteString(", ")
}
@@ -49,27 +82,26 @@ func Classes[T any](hx htmx.HX[T], runs []Run) T {
return hx.Attr("classes", classes.String())
}
-func Add(className string, delay ...time.Duration) classOperation {
+// 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)
}
-func Remove(className string, delay ...time.Duration) classOperation {
+// 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)
}
-func Toggle(className string, delay ...time.Duration) classOperation {
+// 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 {
- var delayValue time.Duration
- if len(delay) > 0 {
- delayValue = delay[0]
- }
-
+func makeOperation(op operation, className string, delay time.Duration) classOperation {
return classOperation{
- Operation: op,
- Class: className,
- Delay: delayValue,
+ operation: op,
+ class: className,
+ delay: delay,
}
}
diff --git a/htmx/ext/classtools/classtools_test.go b/htmx/ext/classtools/classtools_test.go
index 36042f9..0994bb6 100644
--- a/htmx/ext/classtools/classtools_test.go
+++ b/htmx/ext/classtools/classtools_test.go
@@ -11,15 +11,31 @@ import (
var hx = htmx.NewStringAttrs()
func ExampleClasses() {
- attr := classtools.Classes(hx, []classtools.Run{
+ 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{
{
- classtools.Add("foo"),
- classtools.Remove("bar", time.Millisecond*500),
+ // 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, remove bar:500ms & toggle baz:1s'
+ // Output: classes='add foo:500ms, remove bar:0s & toggle baz:1s'
}
diff --git a/htmx/ext/ext.go b/htmx/ext/ext.go
deleted file mode 100644
index e95dcae..0000000
--- a/htmx/ext/ext.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package ext
-
-// type Extension string
-
-// const (
-// ClassTools Extension = "class-tools"
-// )
-
-func ClassTools(class string) string {
- return "class-tools"
-}
From 7b11aef68ca78657f575ccbe38103de7cd2bc377 Mon Sep 17 00:00:00 2001
From: Will Ockelmann-Wagner
Date: Fri, 17 May 2024 01:03:28 -0700
Subject: [PATCH 9/9] document extensions, and extract and test some utils
---
.vscode/settings.json | 5 +-
README.md | 42 +++++++-------
assets/badge.svg | 2 +-
.../extempl/activesearch_templ.go | 1 +
.../bulkupdate/extempl/bulkupdate_templ.go | 1 +
.../classtools_ex/exgom/classtools_ex.gom.go | 1 +
.../extempl/classtools_ex_templ.go | 1 +
.../clicktoedit/extempl/clicktoedit_templ.go | 1 +
.../web/layout/templ/layout/layout_templ.go | 1 +
.../progressbar/extempl/progressbar_templ.go | 1 +
htmx/ext/classtools/classtools.go | 16 +++++
htmx/ext/preload/preload.go | 37 +++++++++---
htmx/ext/removeme/removeme.go | 10 +++-
htmx/htmx.go | 46 ++++-----------
htmx/htmx_test.go | 4 +-
htmx/internal/util/util.go | 29 ++++++++++
htmx/internal/util/util_test.go | 58 +++++++++++++++++++
htmx/swap/swap.go | 10 +---
18 files changed, 186 insertions(+), 80 deletions(-)
create mode 100644 htmx/internal/util/util.go
create mode 100644 htmx/internal/util/util_test.go
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b9018dc..2fbef1d 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,8 +1,5 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast"],
- "cSpell.words": [
- "classtools",
- "gomponents"
- ]
+ "cSpell.words": ["classtools", "gomponents"]
}
diff --git a/README.md b/README.md
index e285c81..aa962fa 100644
--- a/README.md
+++ b/README.md
@@ -65,13 +65,33 @@ templ search() {
}
```
+## Extensions
+
+htmx includes a set of extensions out of the box that address common developer needs. These extensions are tested against htmx in each distribution.
+
+While you can always use any extension by adding standard HTML attributes, `typed-htmx-go` has typed support for some extensions.
+
+These extensions each have their own package, and expose function that take a configured `hx` as a first parameter, and return a full attribute.
+
+`hx` also includes an `hx.Ext()` method to register an extension on an element (ie: `{ hx.Ext(classtools.Extension)... }` instead of `hx-ext="class-tools"`.
+
+### Current Extensions supported
+
+See [htmx/ext](./htmx/ext) for a full list of extensions.
+
+- [`class-tools`](https://htmx.org/extensions/class-tools/)
+- [`preload`](https://htmx.org/extensions/preload/)
+- [`remove-me`](https://htmx.org/extensions/remove-me/)
+
## Examples
Usage examples are in [examples](./examples) (hosted at [typed-htmx-go.vercel.app](https://typed-htmx-go.vercel.app/))
+These are mostly ported from the [HTMX examples](https://htmx.org/examples/), but include a Templ and Gomponents implementation, and working server code to borrow from.
+
## HTMX Version
-`typed-hx-go` strives to keep up with HTMX releases. It currently supports HTMX `v1.9.10`.
+`typed-htmx-go` strives to keep up with HTMX releases. It currently supports HTMX `v1.9.10`.
## Goals
@@ -183,26 +203,6 @@ Form(
Every attribute function should have a test to make sure it's printing valid HTMX. And every function and option should include an example test, to make it easy to see usage in the godocs. These are also a good opportunity to try out the API and make sure it's ergonomic in practice.
-## Notable attributes
-
-Most of the attributes in HTMX are pretty straightforward to use - you just pass in CSS selector that the attribute should apply to, or nothing at all. A few are more complicated though, and are listed here:
-
-### Config
-
-TODO
-
-### On
-
-TODO
-
-### Swap
-
-TODO
-
-### Trigger
-
-TODO
-
## Contributing
### Install Tasklist
diff --git a/assets/badge.svg b/assets/badge.svg
index 11793a0..155d150 100644
--- a/assets/badge.svg
+++ b/assets/badge.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/examples/web/activesearch/extempl/activesearch_templ.go b/examples/web/activesearch/extempl/activesearch_templ.go
index c932b74..c729e98 100644
--- a/examples/web/activesearch/extempl/activesearch_templ.go
+++ b/examples/web/activesearch/extempl/activesearch_templ.go
@@ -13,6 +13,7 @@ import (
"time"
"github.com/a-h/templ"
+
"github.com/will-wow/typed-htmx-go/htmx"
"github.com/will-wow/typed-htmx-go/htmx/trigger"
diff --git a/examples/web/bulkupdate/extempl/bulkupdate_templ.go b/examples/web/bulkupdate/extempl/bulkupdate_templ.go
index 5aa8129..0ffd42a 100644
--- a/examples/web/bulkupdate/extempl/bulkupdate_templ.go
+++ b/examples/web/bulkupdate/extempl/bulkupdate_templ.go
@@ -14,6 +14,7 @@ import (
"github.com/a-h/templ"
"github.com/lithammer/dedent"
+
"github.com/will-wow/typed-htmx-go/htmx"
"github.com/will-wow/typed-htmx-go/htmx/swap"
diff --git a/examples/web/classtools_ex/exgom/classtools_ex.gom.go b/examples/web/classtools_ex/exgom/classtools_ex.gom.go
index e162e5f..3413f06 100644
--- a/examples/web/classtools_ex/exgom/classtools_ex.gom.go
+++ b/examples/web/classtools_ex/exgom/classtools_ex.gom.go
@@ -7,6 +7,7 @@ import (
. "github.com/maragudk/gomponents/html"
g "github.com/maragudk/gomponents"
+
"github.com/will-wow/typed-htmx-go/htmx/ext/classtools"
"github.com/will-wow/typed-htmx-go/examples/web/exprint"
diff --git a/examples/web/classtools_ex/extempl/classtools_ex_templ.go b/examples/web/classtools_ex/extempl/classtools_ex_templ.go
index a9f24b9..22e95e5 100644
--- a/examples/web/classtools_ex/extempl/classtools_ex_templ.go
+++ b/examples/web/classtools_ex/extempl/classtools_ex_templ.go
@@ -13,6 +13,7 @@ import (
"time"
"github.com/a-h/templ"
+
"github.com/will-wow/typed-htmx-go/htmx"
"github.com/will-wow/typed-htmx-go/htmx/ext/classtools"
diff --git a/examples/web/clicktoedit/extempl/clicktoedit_templ.go b/examples/web/clicktoedit/extempl/clicktoedit_templ.go
index 367ea6d..18c9f87 100644
--- a/examples/web/clicktoedit/extempl/clicktoedit_templ.go
+++ b/examples/web/clicktoedit/extempl/clicktoedit_templ.go
@@ -12,6 +12,7 @@ import (
"io"
"github.com/a-h/templ"
+
"github.com/will-wow/typed-htmx-go/htmx"
"github.com/will-wow/typed-htmx-go/htmx/swap"
diff --git a/examples/web/layout/templ/layout/layout_templ.go b/examples/web/layout/templ/layout/layout_templ.go
index 010ecec..a3c6a4f 100644
--- a/examples/web/layout/templ/layout/layout_templ.go
+++ b/examples/web/layout/templ/layout/layout_templ.go
@@ -12,6 +12,7 @@ import (
"time"
"github.com/a-h/templ"
+
"github.com/will-wow/typed-htmx-go/htmx"
"github.com/will-wow/typed-htmx-go/htmx/hxconfig"
)
diff --git a/examples/web/progressbar/extempl/progressbar_templ.go b/examples/web/progressbar/extempl/progressbar_templ.go
index b0c36a3..6d6ffd5 100644
--- a/examples/web/progressbar/extempl/progressbar_templ.go
+++ b/examples/web/progressbar/extempl/progressbar_templ.go
@@ -15,6 +15,7 @@ import (
"time"
"github.com/a-h/templ"
+
"github.com/will-wow/typed-htmx-go/htmx"
"github.com/will-wow/typed-htmx-go/htmx/ext/classtools"
"github.com/will-wow/typed-htmx-go/htmx/swap"
diff --git a/htmx/ext/classtools/classtools.go b/htmx/ext/classtools/classtools.go
index 370c0fb..127a65c 100644
--- a/htmx/ext/classtools/classtools.go
+++ b/htmx/ext/classtools/classtools.go
@@ -8,6 +8,14 @@ import (
)
// 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.
@@ -43,6 +51,10 @@ type Run []classOperation
//
//
//
+//
+// 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})
}
@@ -60,6 +72,10 @@ func Classes[T any](hx htmx.HX[T], operations ...classOperation) T {
// {classtools.Add("foo", time.Second)},
// })... } />
//
+//
+// 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{}
diff --git a/htmx/ext/preload/preload.go b/htmx/ext/preload/preload.go
index f3e5e50..e98afdd 100644
--- a/htmx/ext/preload/preload.go
+++ b/htmx/ext/preload/preload.go
@@ -1,14 +1,31 @@
package preload
-import "github.com/will-wow/typed-htmx-go/htmx"
+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 (
@@ -17,18 +34,20 @@ const (
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", boolToString(preloadImages))
-}
-
-func boolToString(b bool) string {
- if b {
- return "true"
- }
- return "false"
+ return hx.Attr("preload-images", util.BoolToString(preloadImages))
}
diff --git a/htmx/ext/removeme/removeme.go b/htmx/ext/removeme/removeme.go
index 15c5f64..4120cd4 100644
--- a/htmx/ext/removeme/removeme.go
+++ b/htmx/ext/removeme/removeme.go
@@ -8,12 +8,20 @@ import (
// Extension allows you to remove an element after a specified interval.
//
-// extension: [remove-me]
+// # 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/htmx.go b/htmx/htmx.go
index 7cad6de..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.
@@ -192,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.
@@ -294,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 {
@@ -473,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.
//
@@ -675,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.
//
@@ -900,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.
@@ -932,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.
//
@@ -950,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.
//
@@ -1134,7 +1135,7 @@ func (hx *HX[T]) Put(url string, a ...any) 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
@@ -1262,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.
//
@@ -1320,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
@@ -1388,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))
@@ -1420,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 f9b292a..ac67805 100644
--- a/htmx/htmx_test.go
+++ b/htmx/htmx_test.go
@@ -319,8 +319,8 @@ func ExampleHX_Patch() {
}
func ExampleHX_Patch_format() {
- fmt.Println(hx.Patch("/example"))
- // Output: hx-patch='/example'
+ fmt.Println(hx.Patch("/example/%d", 1))
+ // Output: hx-patch='/example/1'
}
func ExampleHX_Preserve() {
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"
-}