From c6f62dce1cc7fd68bc27e4a4c8211afc15b7f7be Mon Sep 17 00:00:00 2001 From: Will Ockelmann-Wagner Date: Sat, 27 Apr 2024 17:11:48 -0700 Subject: [PATCH 1/9] example templ progress bar --- .vscode/examples.code-snippets | 51 +- examples/go.mod | 1 + examples/go.sum | 2 + .../extempl/activesearch_templ.go | 1 - .../bulkupdate/extempl/bulkupdate_templ.go | 1 - .../clicktoedit/extempl/clicktoedit_templ.go | 1 - examples/web/examples/exgom/examples.gom.go | 5 + examples/web/examples/extempl/examples.templ | 11 +- .../web/examples/extempl/examples_templ.go | 18 +- .../web/layout/templ/layout/layout_templ.go | 1 - .../web/progressbar/exgom/progressbar.gom.go | 55 +++ .../web/progressbar/extempl/progressbar.templ | 187 ++++++++ .../progressbar/extempl/progressbar_templ.go | 445 ++++++++++++++++++ examples/web/progressbar/progressbar.go | 152 ++++++ examples/web/progressbar/shared/shared.go | 3 + examples/web/static/main.css | 24 + examples/web/web.go | 20 +- htmx/trigger/poll.go | 2 +- 18 files changed, 939 insertions(+), 41 deletions(-) create mode 100644 examples/web/progressbar/exgom/progressbar.gom.go create mode 100644 examples/web/progressbar/extempl/progressbar.templ create mode 100644 examples/web/progressbar/extempl/progressbar_templ.go create mode 100644 examples/web/progressbar/progressbar.go create mode 100644 examples/web/progressbar/shared/shared.go diff --git a/.vscode/examples.code-snippets b/.vscode/examples.code-snippets index 707c173..cdc6ed5 100644 --- a/.vscode/examples.code-snippets +++ b/.vscode/examples.code-snippets @@ -12,8 +12,6 @@ "body": [ "package ${TM_DIRECTORY/.*\\/(.*)$/$1/}", "", - "package activesearch", - "", "import (", " \"net/http\"", "", @@ -45,36 +43,42 @@ "scope": "templ", "prefix": "page", "body": [ - "package ${TM_DIRECTORY/.*\\/(.*)$/$1/}", + "package \"extempl\"", "", "import (", " \"embed\"", "", " \"github.com/will-wow/typed-htmx-go/examples/web/layout/templ/layout\"", - " \"github.com/will-wow/typed-htmx-go/examples/web/${$2}/shared\"", + " \"github.com/will-wow/typed-htmx-go/examples/web/${TM_DIRECTORY/.*\\/(.*)$/$1/}/shared\"", " \"github.com/will-wow/typed-htmx-go/examples/web/exprint\"", " \"github.com/will-wow/typed-htmx-go/htmx\"", ")", "", - "", "var hx = htmx.NewTempl()", "", - "//go:embed activesearch.templ", + "//go:embed ${TM_DIRECTORY/.*\\/(.*)$/$1/}.templ", "var fs embed.FS", "var ex = exprint.New(fs, \"//\", \"\")", "", "templ Page() {", " @layout.Base(\"$1\") {", - "

$1

", - "

", - " Desc", - "

", - "
",
-      "			",
-      "   		{ ex.PrintOrErr(\"$2.templ\", \"$2\") }",
-      "			",
-      "		
", - " }", + "

$1

", + "

", + " Desc", + "

", + "
",
+      "	 		",
+      "    		{ ex.PrintOrErr(\"${TM_DIRECTORY/.*\\/(.*)$/$1/}.templ\", \"demo\") }",
+      "	 		",
+      "	 	
", + "

Demo

", + " { demo() }", + " }", + "}", + "", + "templ demo() {", + " //ex:start:demo", + " //ex:end:demo", "}", ], }, @@ -99,7 +103,7 @@ "", "var hx = htmx.NewGomponents()", "", - "//go:embed activesearch.gom.go", + "//go:embed ${TM_DIRECTORY/.*\\/(.*)$/$1/}.gom.go", "var fs embed.FS", "var ex = exprint.New(fs, \"//\", \"\")", "", @@ -113,9 +117,18 @@ " Pre(", " Code(", " Class(\"language-go\"),", - " g.Text(ex.PrintOrErr(\"$2.gom.go\", \"$2\")),", + " g.Text(ex.PrintOrErr(\"$TM_FILENAME_BASE.gom.go\", \"demo\")),", " ),", - " ),", + " ),", + " H2(g.Text(\"Demo\")),", + " demo(),", + " )", + "}", + "", + "func demo() g.Node {", + " //ex:start:demo", + " //ex:end:demo", + "}", ], "description": "gom", }, diff --git a/examples/go.mod b/examples/go.mod index 01c456b..13f2d17 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -6,6 +6,7 @@ require github.com/a-h/templ v0.2.663 require ( github.com/PuerkitoBio/goquery v1.8.1 + github.com/angelofallars/htmx-go v0.5.0 github.com/lithammer/dedent v1.1.0 github.com/maragudk/gomponents v0.20.2 github.com/will-wow/typed-htmx-go v0.1.0 diff --git a/examples/go.sum b/examples/go.sum index 7d502e5..9890632 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -4,6 +4,8 @@ github.com/a-h/templ v0.2.663 h1:aa0WMm27InkYHGjimcM7us6hJ6BLhg98ZbfaiDPyjHE= github.com/a-h/templ v0.2.663/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/angelofallars/htmx-go v0.5.0 h1:L7M48cCH7nX8cV5wRYn04pN6AE4qNdh86iTbuKxhnIo= +github.com/angelofallars/htmx-go v0.5.0/go.mod h1:izXk6A+Jllc3vXs1dUvxUJs/jE0weiEC07ZPlCVi4cc= 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/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= diff --git a/examples/web/activesearch/extempl/activesearch_templ.go b/examples/web/activesearch/extempl/activesearch_templ.go index c729e98..c932b74 100644 --- a/examples/web/activesearch/extempl/activesearch_templ.go +++ b/examples/web/activesearch/extempl/activesearch_templ.go @@ -13,7 +13,6 @@ 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 0ffd42a..5aa8129 100644 --- a/examples/web/bulkupdate/extempl/bulkupdate_templ.go +++ b/examples/web/bulkupdate/extempl/bulkupdate_templ.go @@ -14,7 +14,6 @@ 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/clicktoedit/extempl/clicktoedit_templ.go b/examples/web/clicktoedit/extempl/clicktoedit_templ.go index 18c9f87..367ea6d 100644 --- a/examples/web/clicktoedit/extempl/clicktoedit_templ.go +++ b/examples/web/clicktoedit/extempl/clicktoedit_templ.go @@ -12,7 +12,6 @@ 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/examples/exgom/examples.gom.go b/examples/web/examples/exgom/examples.gom.go index e3b447d..950a4a4 100644 --- a/examples/web/examples/exgom/examples.gom.go +++ b/examples/web/examples/exgom/examples.gom.go @@ -44,6 +44,11 @@ func Page() g.Node { "Active Search", "Demonstrates the active search box pattern", ), + exampleRow( + "/examples/gomponents/progress-bar", + "Progress Bar", + "Demonstrates a job-runner like progress bar", + ), ), ), ) diff --git a/examples/web/examples/extempl/examples.templ b/examples/web/examples/extempl/examples.templ index 728065e..c5dafed 100644 --- a/examples/web/examples/extempl/examples.templ +++ b/examples/web/examples/extempl/examples.templ @@ -33,20 +33,25 @@ templ Page() { @exampleRow( - "/examples/templ/click-to-edit", + "/examples/templ/click-to-edit/", "Click To Edit", "Demonstrates inline editing of a data object", ) @exampleRow( - "/examples/templ/bulk-update", + "/examples/templ/bulk-update/", "Bulk Update", "Demonstrates bulk updating of multiple rows of data", ) @exampleRow( - "/examples/templ/active-search", + "/examples/templ/active-search/", "Active Search", "Demonstrates the active search box pattern", ) + @exampleRow( + "/examples/templ/progress-bar/", + "Progress Bar", + "Demonstrates a job-runner like progress bar", + ) } diff --git a/examples/web/examples/extempl/examples_templ.go b/examples/web/examples/extempl/examples_templ.go index 48b01d0..382e0b4 100644 --- a/examples/web/examples/extempl/examples_templ.go +++ b/examples/web/examples/extempl/examples_templ.go @@ -39,7 +39,7 @@ func Page() templ.Component { return templ_7745c5c3_Err } templ_7745c5c3_Err = exampleRow( - "/examples/templ/click-to-edit", + "/examples/templ/click-to-edit/", "Click To Edit", "Demonstrates inline editing of a data object", ).Render(ctx, templ_7745c5c3_Buffer) @@ -47,7 +47,7 @@ func Page() templ.Component { return templ_7745c5c3_Err } templ_7745c5c3_Err = exampleRow( - "/examples/templ/bulk-update", + "/examples/templ/bulk-update/", "Bulk Update", "Demonstrates bulk updating of multiple rows of data", ).Render(ctx, templ_7745c5c3_Buffer) @@ -55,13 +55,21 @@ func Page() templ.Component { return templ_7745c5c3_Err } templ_7745c5c3_Err = exampleRow( - "/examples/templ/active-search", + "/examples/templ/active-search/", "Active Search", "Demonstrates the active search box pattern", ).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + templ_7745c5c3_Err = exampleRow( + "/examples/templ/progress-bar/", + "Progress Bar", + "Demonstrates a job-runner like progress bar", + ).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 @@ -111,7 +119,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: 58, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/extempl/examples.templ`, Line: 63, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -124,7 +132,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: 61, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/extempl/examples.templ`, Line: 66, Col: 16} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { diff --git a/examples/web/layout/templ/layout/layout_templ.go b/examples/web/layout/templ/layout/layout_templ.go index 959b677..e535dc0 100644 --- a/examples/web/layout/templ/layout/layout_templ.go +++ b/examples/web/layout/templ/layout/layout_templ.go @@ -12,7 +12,6 @@ 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/exgom/progressbar.gom.go b/examples/web/progressbar/exgom/progressbar.gom.go new file mode 100644 index 0000000..f3eec4d --- /dev/null +++ b/examples/web/progressbar/exgom/progressbar.gom.go @@ -0,0 +1,55 @@ +package exgom + +import ( + "embed" + + g "github.com/maragudk/gomponents" + . "github.com/maragudk/gomponents/html" + + "github.com/will-wow/typed-htmx-go/htmx/swap" + + "github.com/will-wow/typed-htmx-go/examples/web/exprint" + "github.com/will-wow/typed-htmx-go/examples/web/layout/gom/layout" + + "github.com/will-wow/typed-htmx-go/htmx" +) + +var hx = htmx.NewGomponents() + +//go:embed progressbar.gom.go +var fs embed.FS +var ex = exprint.New(fs, "//", "") + +func Page() g.Node { + return layout.Wrapper( + "Progress Bar", + Class("progress-bar"), + H1(g.Text("Progress Bar")), + P( + g.Text("This example shows how to implement a smoothly scrolling progress bar."), + ), + Pre( + Code( + Class("language-go"), + g.Text(ex.PrintOrErr("progressbar.gom.go", "demo")), + ), + ), + H2(g.Text("Demo")), + demo(), + ) +} + +func demo() g.Node { + //ex:start:demo + return Div( + hx.Target(htmx.TargetThis), + hx.Swap(swap.OuterHTML), + H3(g.Text("Start Progress")), + Button( + Class("btn"), + hx.Post("/examples/templ/progress-bar/job/"), + g.Text("Start Job"), + ), + ) + //ex:end:demo +} diff --git a/examples/web/progressbar/extempl/progressbar.templ b/examples/web/progressbar/extempl/progressbar.templ new file mode 100644 index 0000000..d733c15 --- /dev/null +++ b/examples/web/progressbar/extempl/progressbar.templ @@ -0,0 +1,187 @@ +package extempl + +import ( + "fmt" + "embed" + "strconv" + "time" + + "github.com/will-wow/typed-htmx-go/examples/web/layout/templ/layout" + "github.com/will-wow/typed-htmx-go/examples/web/exprint" + "github.com/will-wow/typed-htmx-go/examples/web/progressbar/shared" + "github.com/will-wow/typed-htmx-go/htmx/swap" + "github.com/will-wow/typed-htmx-go/htmx" + "github.com/will-wow/typed-htmx-go/htmx/trigger" +) + +var hx = htmx.NewTempl() + +//go:embed progressbar.templ +var fs embed.FS +var ex = exprint.New(fs, "//", "") + +templ Page() { + @layout.Wrapper("Progress Bar", "progress-bar") { +

Progress Bar

+

+ This example shows how to implement a smoothly scrolling progress bar. +

+

+ We start with an initial state with a button that issues a POST to /start to begin the job: +

+
+			
+				{ ex.PrintOrErr("progressbar.templ", "start") }
+			
+		
+

+ This div is then replaced with a new div containing status and a progress bar that reloads itself every 600ms: +

+
+			
+				{ ex.PrintOrErr("progressbar.templ", "running") }
+			
+		
+
+			
+				{ ex.PrintOrErr("progressbar.templ", "progress") }
+			
+		
+

+ 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): +

+
+			
+				{ ex.PrintOrErr("progressbar.templ", "done") }
+			
+		
+

+ This example uses styling cribbed from the bootstrap progress bar: +

+
+			
+				{ styleExample }
+			
+		
+

Demo

+ @Start() + } +} + +templ Start() { + //ex:start:start +
+

Start Progress

+ +
+ //ex:end:start +} + +templ JobRunning(jobID int64, progress int) { + //ex:start:running +
+

+ Job { strconv.FormatInt(jobID, 10) } Running +

+ @ProgressFetcher(jobID, progress) +
+ //ex:end:running +} + +templ Job(jobID int64, progress int) { + //ex:start:done +
+

+ Job { strconv.FormatInt(jobID, 10) } Complete +

+ @ProgressBar(progress) + +
+ //ex:end:done +} + +//ex:start:progress +templ ProgressFetcher(jobID int64, progress int) { +
+ @ProgressBar(progress) +
+} + +templ ProgressBar(progress int) { +
+
+
+} + +func progressWidth(percent int) templ.Attributes { + return templ.Attributes{ + "style": fmt.Sprintf("width: %d%%", percent), + } +} + +//ex:end:progress + +var styleExample = ` +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + box-shadow: inset 0 1px 2px rgba(0,0,0,.1); +} +.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,.15); + box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} +` diff --git a/examples/web/progressbar/extempl/progressbar_templ.go b/examples/web/progressbar/extempl/progressbar_templ.go new file mode 100644 index 0000000..ad46955 --- /dev/null +++ b/examples/web/progressbar/extempl/progressbar_templ.go @@ -0,0 +1,445 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.663 +package extempl + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "embed" + "fmt" + "io" + "strconv" + "time" + + "github.com/a-h/templ" + "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" + + "github.com/will-wow/typed-htmx-go/examples/web/exprint" + "github.com/will-wow/typed-htmx-go/examples/web/layout/templ/layout" + "github.com/will-wow/typed-htmx-go/examples/web/progressbar/shared" +) + +var hx = htmx.NewTempl() + +//go:embed progressbar.templ +var fs embed.FS +var ex = exprint.New(fs, "//", "") + +func 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("

Progress Bar

This example shows how to implement a smoothly scrolling progress bar.

We start with an initial state with a button that issues a POST to /start to begin the job:

")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var3 string
+			templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "start"))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 34, Col: 49}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

This div is then replaced with a new div containing status and a progress bar that reloads itself every 600ms:

")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var4 string
+			templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "running"))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 42, Col: 51}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+			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_Var5 string
+			templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "progress"))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 47, Col: 52}
+			}
+			_, 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("

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

")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var6 string
+			templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "done"))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 58, Col: 48}
+			}
+			_, 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("

This example uses styling cribbed from the bootstrap progress bar:

")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var7 string
+			templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(styleExample)
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 66, Col: 18}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Demo

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Start().Render(ctx, templ_7745c5c3_Buffer) + 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 = layout.Wrapper("Progress Bar", "progress-bar").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 Start() 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_Var8 := templ.GetChildren(ctx) + if templ_7745c5c3_Var8 == nil { + templ_7745c5c3_Var8 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Start Progress

") + 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 JobRunning(jobID int64, progress int) 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_Err = templ_7745c5c3_Buffer.WriteString("

Job ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(jobID, 10)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 97, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" Running

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ProgressFetcher(jobID, progress).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 = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func Job(jobID int64, progress int) 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_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Job ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(jobID, 10)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 111, Col: 37} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" Complete

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ProgressBar(progress).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 = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +//ex:start:progress +func ProgressFetcher(jobID int64, progress int) 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_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ProgressBar(progress).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 = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func ProgressBar(progress int) 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_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, 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 progressWidth(percent int) templ.Attributes { + return templ.Attributes{ + "style": fmt.Sprintf("width: %d%%", percent), + } +} + +//ex:end:progress + +var styleExample = ` +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + box-shadow: inset 0 1px 2px rgba(0,0,0,.1); +} +.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,.15); + box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} +` diff --git a/examples/web/progressbar/progressbar.go b/examples/web/progressbar/progressbar.go new file mode 100644 index 0000000..1818500 --- /dev/null +++ b/examples/web/progressbar/progressbar.go @@ -0,0 +1,152 @@ +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 { + component := extempl.Page() + _ = component.Render(r.Context(), w) + } +} + +func (ex *example) start(w http.ResponseWriter, r *http.Request) { + id := ex.jobs.add() + component := extempl.JobRunning(id, 0) + _ = component.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)) + } + + 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 + } + + res := htmx.NewResponse() + res.MustRenderTempl(r.Context(), w, extempl.Job(id, progress)) +} + +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/main.css b/examples/web/static/main.css index b408211..ad7b056 100644 --- a/examples/web/static/main.css +++ b/examples/web/static/main.css @@ -19,3 +19,27 @@ .active-search.htmx-indicator.htmx-request { opacity: 1; } + +.progress-bar .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 .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; +} diff --git a/examples/web/web.go b/examples/web/web.go index 69a267a..65e4e94 100644 --- a/examples/web/web.go +++ b/examples/web/web.go @@ -11,6 +11,7 @@ import ( "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" + "github.com/will-wow/typed-htmx-go/examples/web/progressbar" ) //go:embed "static" @@ -50,19 +51,20 @@ 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) 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/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, From 16e99dcb7a75ccb8905cb177d1590d89ae4f6aa7 Mon Sep 17 00:00:00 2001 From: Will Ockelmann-Wagner Date: Sat, 27 Apr 2024 17:18:53 -0700 Subject: [PATCH 2/9] update deps --- examples/Taskfile.yml | 4 +-- examples/go.mod | 8 +++--- examples/go.sum | 28 +++++++++++-------- .../extempl/activesearch_templ.go | 1 + .../bulkupdate/extempl/bulkupdate_templ.go | 1 + .../clicktoedit/extempl/clicktoedit_templ.go | 1 + .../web/layout/templ/layout/layout_templ.go | 1 + .../progressbar/extempl/progressbar_templ.go | 1 + go.work.sum | 4 ++- 9 files changed, 30 insertions(+), 19 deletions(-) diff --git a/examples/Taskfile.yml b/examples/Taskfile.yml index 47a786b..8e55387 100644 --- a/examples/Taskfile.yml +++ b/examples/Taskfile.yml @@ -25,8 +25,8 @@ tasks: fmt: desc: Run goimports cmds: - # - go mod tidy - - go run golang.org/x/tools/cmd/goimports@v0.18.0 -w -local github.com/will-wow/typed-htmx-go/examples . + - go mod tidy + - go run golang.org/x/tools/cmd/goimports@v0.20.0 -w -local github.com/will-wow/typed-htmx-go/examples . test: desc: Run test suite diff --git a/examples/go.mod b/examples/go.mod index 13f2d17..dc8e48e 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -5,14 +5,14 @@ go 1.22.0 require github.com/a-h/templ v0.2.663 require ( - github.com/PuerkitoBio/goquery v1.8.1 + github.com/PuerkitoBio/goquery v1.9.1 github.com/angelofallars/htmx-go v0.5.0 github.com/lithammer/dedent v1.1.0 github.com/maragudk/gomponents v0.20.2 - github.com/will-wow/typed-htmx-go v0.1.0 + github.com/will-wow/typed-htmx-go v0.1.1 ) require ( - github.com/andybalholm/cascadia v1.3.1 // indirect - golang.org/x/net v0.19.0 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect + golang.org/x/net v0.24.0 // indirect ) diff --git a/examples/go.sum b/examples/go.sum index 9890632..374c833 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,9 +1,9 @@ -github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= -github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= +github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= github.com/a-h/templ v0.2.663 h1:aa0WMm27InkYHGjimcM7us6hJ6BLhg98ZbfaiDPyjHE= github.com/a-h/templ v0.2.663/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8= -github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= -github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/angelofallars/htmx-go v0.5.0 h1:L7M48cCH7nX8cV5wRYn04pN6AE4qNdh86iTbuKxhnIo= github.com/angelofallars/htmx-go v0.5.0/go.mod h1:izXk6A+Jllc3vXs1dUvxUJs/jE0weiEC07ZPlCVi4cc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -12,37 +12,41 @@ github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffkt github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/maragudk/gomponents v0.20.2 h1:39FhnBNNCJzqNcD9Hmvp/5xj0otweFoyvVgFG6kXoy0= github.com/maragudk/gomponents v0.20.2/go.mod h1:nHkNnZL6ODgMBeJhrZjkMHVvNdoYsfmpKB2/hjdQ0Hg= -github.com/will-wow/typed-htmx-go v0.1.0 h1:KgWRl4SiRI+c8RuOKp9XxdQkPxZBrLtHQKEMto2vD+Q= -github.com/will-wow/typed-htmx-go v0.1.0/go.mod h1:74VnqtHJBD+KHLksfxCDYROQhEI4OUcb0iOJEuOBVvs= +github.com/will-wow/typed-htmx-go v0.1.1 h1:Ow2kFDh35S6SplP1uD/gIYKQARm3n3gUmqp83wByvTg= +github.com/will-wow/typed-htmx-go v0.1.1/go.mod h1:4kTdRyJEy/oSURNcUAUvSiJ90Mf19W0dEhe/ouK7530= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/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 e535dc0..959b677 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 ad46955..0a5411a 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/swap" "github.com/will-wow/typed-htmx-go/htmx/trigger" 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= From 9a5ab513f5146707eb7096d9d7fee0ba16747ade Mon Sep 17 00:00:00 2001 From: Will Ockelmann-Wagner Date: Sun, 28 Apr 2024 23:41:04 -0700 Subject: [PATCH 3/9] support format strings for url methods --- htmx/htmx.go | 39 ++++++++++++++++++++++++++++++++------- htmx/htmx_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/htmx/htmx.go b/htmx/htmx.go index 0cbcb1a..2d5c0a6 100644 --- a/htmx/htmx.go +++ b/htmx/htmx.go @@ -92,7 +92,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 +118,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) } @@ -210,7 +216,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) } @@ -635,7 +645,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) } @@ -1020,7 +1034,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 +1096,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) } @@ -1129,7 +1150,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) } diff --git a/htmx/htmx_test.go b/htmx/htmx_test.go index 9fbe35f..f9b292a 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")) + // Output: hx-patch='/example' +} + 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, From ef46b5e435232b3568a8d24cf2742997b0476152 Mon Sep 17 00:00:00 2001 From: Will Ockelmann-Wagner Date: Sun, 28 Apr 2024 23:42:23 -0700 Subject: [PATCH 4/9] use real css for examples --- .../web/progressbar/extempl/progressbar.templ | 49 ++++----------- .../progressbar/extempl/progressbar_templ.go | 60 ++++++------------- examples/web/static/ex.go | 12 ++++ examples/web/static/main.css | 6 +- 4 files changed, 45 insertions(+), 82 deletions(-) create mode 100644 examples/web/static/ex.go diff --git a/examples/web/progressbar/extempl/progressbar.templ b/examples/web/progressbar/extempl/progressbar.templ index d733c15..dc7577a 100644 --- a/examples/web/progressbar/extempl/progressbar.templ +++ b/examples/web/progressbar/extempl/progressbar.templ @@ -8,6 +8,7 @@ import ( "github.com/will-wow/typed-htmx-go/examples/web/layout/templ/layout" "github.com/will-wow/typed-htmx-go/examples/web/exprint" + "github.com/will-wow/typed-htmx-go/examples/web/static" "github.com/will-wow/typed-htmx-go/examples/web/progressbar/shared" "github.com/will-wow/typed-htmx-go/htmx/swap" "github.com/will-wow/typed-htmx-go/htmx" @@ -21,7 +22,7 @@ var fs embed.FS var ex = exprint.New(fs, "//", "") templ Page() { - @layout.Wrapper("Progress Bar", "progress-bar") { + @layout.Wrapper("Progress Bar", "progress-bar-demo") {

Progress Bar

This example shows how to implement a smoothly scrolling progress bar. @@ -31,7 +32,7 @@ templ Page() {

 			
-				{ ex.PrintOrErr("progressbar.templ", "start") }
+				{ ex.PrintOrErr("progressbar.templ", "demo") }
 			
 		

@@ -48,10 +49,10 @@ templ Page() {

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

 			
@@ -63,16 +64,16 @@ templ Page() {
 		

 			
-				{ styleExample }
+				{ static.ExCSS.PrintOrErr("main.css", "progress-bar-style") }
 			
 		

Demo

- @Start() + @demo() } } -templ Start() { - //ex:start:start +templ demo() { + //ex:start:demo
- //ex:end:start + //ex:end:demo } templ JobRunning(jobID int64, progress int) { //ex:start:running
@@ -126,7 +127,7 @@ templ Job(jobID int64, progress int) { //ex:start:progress templ ProgressFetcher(jobID int64, progress int) {

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

")
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

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
 			}
 			var templ_7745c5c3_Var6 string
 			templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "done"))
 			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 58, Col: 48}
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 59, Col: 48}
 			}
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
 			if templ_7745c5c3_Err != nil {
@@ -107,9 +107,9 @@ func Page() templ.Component {
 				return templ_7745c5c3_Err
 			}
 			var templ_7745c5c3_Var7 string
-			templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(styleExample)
+			templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(static.ExCSS.PrintOrErr("main.css", "progress-bar-style"))
 			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 66, Col: 18}
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 67, Col: 63}
 			}
 			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
 			if templ_7745c5c3_Err != nil {
@@ -119,7 +119,7 @@ func Page() templ.Component {
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
-			templ_7745c5c3_Err = Start().Render(ctx, templ_7745c5c3_Buffer)
+			templ_7745c5c3_Err = demo().Render(ctx, templ_7745c5c3_Buffer)
 			if templ_7745c5c3_Err != nil {
 				return templ_7745c5c3_Err
 			}
@@ -128,7 +128,7 @@ func Page() templ.Component {
 			}
 			return templ_7745c5c3_Err
 		})
-		templ_7745c5c3_Err = layout.Wrapper("Progress Bar", "progress-bar").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+		templ_7745c5c3_Err = layout.Wrapper("Progress Bar", "progress-bar-demo").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
@@ -139,7 +139,7 @@ func Page() templ.Component {
 	})
 }
 
-func Start() templ.Component {
+func demo() 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 {
@@ -204,7 +204,7 @@ func JobRunning(jobID int64, progress int) templ.Component {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, hx.Get(fmt.Sprintf("/examples/templ/progress-bar/job/%d/", jobID)))
+		templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, hx.Get("/examples/templ/progress-bar/job/%d/", jobID))
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
@@ -223,7 +223,7 @@ func JobRunning(jobID int64, progress int) templ.Component {
 		var templ_7745c5c3_Var10 string
 		templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(jobID, 10))
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 97, Col: 37}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 98, Col: 37}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
 		if templ_7745c5c3_Err != nil {
@@ -280,7 +280,7 @@ func Job(jobID int64, progress int) templ.Component {
 		var templ_7745c5c3_Var12 string
 		templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatInt(jobID, 10))
 		if templ_7745c5c3_Err != nil {
-			return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 111, Col: 37}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 112, Col: 37}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
 		if templ_7745c5c3_Err != nil {
@@ -331,7 +331,7 @@ func ProgressFetcher(jobID int64, progress int) templ.Component {
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
-		templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, hx.Get(fmt.Sprintf("/examples/templ/progress-bar/job/%d/progress/", jobID)))
+		templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, hx.Get("/examples/templ/progress-bar/job/%d/progress/", jobID))
 		if templ_7745c5c3_Err != nil {
 			return templ_7745c5c3_Err
 		}
@@ -386,7 +386,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: 144, Col: 40}
+			return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 145, Col: 40}
 		}
 		_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
 		if templ_7745c5c3_Err != nil {
@@ -418,29 +418,3 @@ func progressWidth(percent int) templ.Attributes {
 }
 
 //ex:end:progress
-
-var styleExample = `
-.progress {
-    height: 20px;
-    margin-bottom: 20px;
-    overflow: hidden;
-    background-color: #f5f5f5;
-    border-radius: 4px;
-    box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
-}
-.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,.15);
-    box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
-    -webkit-transition: width .6s ease;
-    -o-transition: width .6s ease;
-    transition: width .6s ease;
-}
-`
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 ad7b056..dcab66c 100644
--- a/examples/web/static/main.css
+++ b/examples/web/static/main.css
@@ -20,7 +20,8 @@
   opacity: 1;
 }
 
-.progress-bar .progress {
+/*ex:start:progress-bar-style*/
+.progress-bar-demo .progress {
   height: 20px;
   margin-bottom: 20px;
   overflow: hidden;
@@ -28,7 +29,7 @@
   border-radius: 4px;
   box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
 }
-.progress-bar .progress-bar {
+.progress-bar-demo .progress-bar {
   float: left;
   width: 0%;
   height: 100%;
@@ -43,3 +44,4 @@
   -o-transition: width 0.6s ease;
   transition: width 0.6s ease;
 }
+/*ex:end:progress-bar-style*/

From 88a8b6a1444f664921e8874398dfa56d7b15699c Mon Sep 17 00:00:00 2001
From: Will Ockelmann-Wagner 
Date: Mon, 29 Apr 2024 00:00:18 -0700
Subject: [PATCH 5/9] example gomponents progress bar

---
 .../web/progressbar/exgom/progressbar.gom.go  | 115 +++++++++++++++++-
 .../progressbar/extempl/progressbar_templ.go  |   1 +
 examples/web/progressbar/progressbar.go       |  25 ++--
 3 files changed, 133 insertions(+), 8 deletions(-)

diff --git a/examples/web/progressbar/exgom/progressbar.gom.go b/examples/web/progressbar/exgom/progressbar.gom.go
index f3eec4d..b7b67d9 100644
--- a/examples/web/progressbar/exgom/progressbar.gom.go
+++ b/examples/web/progressbar/exgom/progressbar.gom.go
@@ -2,14 +2,20 @@ package exgom
 
 import (
 	"embed"
+	"fmt"
+	"strconv"
+	"time"
 
 	g "github.com/maragudk/gomponents"
 	. "github.com/maragudk/gomponents/html"
 
 	"github.com/will-wow/typed-htmx-go/htmx/swap"
+	"github.com/will-wow/typed-htmx-go/htmx/trigger"
 
 	"github.com/will-wow/typed-htmx-go/examples/web/exprint"
 	"github.com/will-wow/typed-htmx-go/examples/web/layout/gom/layout"
+	"github.com/will-wow/typed-htmx-go/examples/web/progressbar/shared"
+	"github.com/will-wow/typed-htmx-go/examples/web/static"
 
 	"github.com/will-wow/typed-htmx-go/htmx"
 )
@@ -23,7 +29,7 @@ var ex = exprint.New(fs, "//", "")
 func Page() g.Node {
 	return layout.Wrapper(
 		"Progress Bar",
-		Class("progress-bar"),
+		Class("progress-bar-demo"),
 		H1(g.Text("Progress Bar")),
 		P(
 			g.Text("This example shows how to implement a smoothly scrolling progress bar."),
@@ -34,6 +40,48 @@ func Page() g.Node {
 				g.Text(ex.PrintOrErr("progressbar.gom.go", "demo")),
 			),
 		),
+		P(g.Text("This div is then replaced with a new div containing status and a progress bar that reloads itself every 600ms:")),
+		Pre(
+			Code(
+				Class("language-go"),
+				g.Text(ex.PrintOrErr("progressbar.gom.go", "running")),
+			),
+		),
+		Pre(
+			Code(
+				Class("language-go"),
+				g.Text(ex.PrintOrErr("progressbar.gom.go", "progress")),
+			),
+		),
+		P(
+			g.Text("This progress bar is updated every 600 milliseconds, with the"),
+			Code(g.Text("width")),
+			g.Text("style attribute and "),
+			Code(g.Text("aria-valuenow")),
+			g.Text("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."),
+		),
+		P(
+			g.Text("Finally, when the process is complete, a server returns a"),
+			Code(g.Text("HX-Trigger: done")),
+			g.Text("header, which triggers an update of the UI to “Complete” state with a restart button added to the UI (we are using the"),
+			Code(g.Text("class-tools")),
+			g.Text("extension in this example to add fade-in effect on the button):"),
+		),
+		Pre(
+			Code(
+				Class("language-go"),
+				g.Text(ex.PrintOrErr("progressbar.gom.go", "done")),
+			),
+		),
+		P(
+			g.Text("This example uses styling cribbed from the bootstrap progress bar:"),
+		),
+		Pre(
+			Code(
+				Class("language-css"),
+				g.Text(static.ExCSS.PrintOrErr("main.css", "progress-bar-style")),
+			),
+		),
 		H2(g.Text("Demo")),
 		demo(),
 	)
@@ -53,3 +101,68 @@ func demo() g.Node {
 	)
 	//ex:end:demo
 }
+
+func JobRunning(jobID int64, progress int) g.Node {
+	//ex:start:running
+	return Div(
+		hx.Trigger(shared.TriggerDone),
+		hx.Get("/examples/templ/progress-bar/job/%d/", jobID),
+		hx.Swap(swap.OuterHTML),
+		hx.Target(htmx.TargetThis),
+		H3(Role("status"), ID("pblabel"), TabIndex("-1"), AutoFocus(),
+			g.Text(fmt.Sprintf("Job %d Running", jobID)),
+		),
+		ProgressFetcher(jobID, progress),
+	)
+	//ex:end:running
+}
+
+func Job(jobID int64, progress int) g.Node {
+	//ex:start:done
+	return Div(
+		hx.Target(htmx.TargetThis),
+		hx.Swap(swap.OuterHTML),
+		H3(Role("status"), ID("pblabel"), TabIndex("-1"), AutoFocus(),
+			g.Text(fmt.Sprintf("Job %d Complete", jobID)),
+		),
+		ProgressBar(progress),
+		Button(
+			ID("restart-btn"),
+			Class("btn"),
+			hx.Post("/examples/templ/progress-bar/job/"),
+			g.Attr("classes", "add show:600ms"),
+			g.Text("Restart Job"),
+		),
+	)
+	//ex:end:done
+}
+
+//ex:start:progress
+func ProgressFetcher(jobID int64, progress int) g.Node {
+	return Div(
+		hx.Get("/examples/templ/progress-bar/job/%d/progress/", jobID),
+		hx.TriggerExtended(trigger.Every(time.Millisecond*600)),
+		hx.Target(htmx.TargetThis),
+		hx.Swap(swap.InnerHTML),
+
+		ProgressBar(progress),
+	)
+}
+
+func ProgressBar(progress int) g.Node {
+	return Div(
+		Class("progress"),
+		Role("progressbar"),
+		Aria("valuemin", "0"),
+		Aria("valuemax", "100"),
+		Aria("valuenow", strconv.Itoa(progress)),
+		Aria("labelledby", "pblabel"),
+		Div(
+			ID("pb"),
+			Class("progress-bar"),
+			// { progressWidth(progress)... }
+		),
+	)
+}
+
+//ex:end:progress
diff --git a/examples/web/progressbar/extempl/progressbar_templ.go b/examples/web/progressbar/extempl/progressbar_templ.go
index 5097d31..0795f00 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/swap"
 	"github.com/will-wow/typed-htmx-go/htmx/trigger"
diff --git a/examples/web/progressbar/progressbar.go b/examples/web/progressbar/progressbar.go
index 1818500..f42e495 100644
--- a/examples/web/progressbar/progressbar.go
+++ b/examples/web/progressbar/progressbar.go
@@ -39,15 +39,18 @@ func (ex *example) demo(w http.ResponseWriter, r *http.Request) {
 	if ex.gom {
 		_ = exgom.Page().Render(w)
 	} else {
-		component := extempl.Page()
-		_ = component.Render(r.Context(), w)
+		_ = extempl.Page().Render(r.Context(), w)
 	}
 }
 
 func (ex *example) start(w http.ResponseWriter, r *http.Request) {
 	id := ex.jobs.add()
-	component := extempl.JobRunning(id, 0)
-	_ = component.Render(r.Context(), w)
+
+	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) {
@@ -75,7 +78,12 @@ func (ex *example) progress(w http.ResponseWriter, r *http.Request) {
 		res = res.AddTrigger(htmx.Trigger(shared.TriggerDone))
 	}
 
-	res.MustRenderTempl(r.Context(), w, extempl.ProgressBar(progress))
+	if ex.gom {
+		res.MustWrite(w)
+		_ = exgom.Page().Render(w)
+	} else {
+		res.MustRenderTempl(r.Context(), w, extempl.ProgressBar(progress))
+	}
 }
 
 func (ex *example) job(w http.ResponseWriter, r *http.Request) {
@@ -97,8 +105,11 @@ func (ex *example) job(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	res := htmx.NewResponse()
-	res.MustRenderTempl(r.Context(), w, extempl.Job(id, progress))
+	if ex.gom {
+		_ = exgom.Page().Render(w)
+	} else {
+		_ = extempl.Job(id, progress).Render(r.Context(), w)
+	}
 }
 
 type job struct {

From c469c338ae1b06a888e0224e002bfe094e51bc6a Mon Sep 17 00:00:00 2001
From: Will Ockelmann-Wagner 
Date: Mon, 29 Apr 2024 01:09:04 -0700
Subject: [PATCH 6/9] add class-tools extension support

---
 .../extempl/activesearch_templ.go             |  1 -
 .../bulkupdate/extempl/bulkupdate_templ.go    |  1 -
 .../clicktoedit/extempl/clicktoedit_templ.go  |  1 -
 examples/web/layout/gom/layout/layout.gom.go  |  1 +
 examples/web/layout/templ/layout/layout.templ |  1 +
 .../web/layout/templ/layout/layout_templ.go   |  3 +-
 .../web/progressbar/exgom/progressbar.gom.go  | 22 +++---
 .../web/progressbar/extempl/progressbar.templ | 13 +++-
 .../progressbar/extempl/progressbar_templ.go  | 48 ++++++++----
 examples/web/progressbar/progressbar.go       |  4 +-
 examples/web/static/main.css                  |  7 ++
 htmx/ext/classtools/classtools.go             | 75 +++++++++++++++++++
 htmx/ext/classtools/classtools_test.go        | 25 +++++++
 htmx/ext/ext.go                               | 11 +++
 htmx/htmx.go                                  | 19 ++++-
 15 files changed, 197 insertions(+), 35 deletions(-)
 create mode 100644 htmx/ext/classtools/classtools.go
 create mode 100644 htmx/ext/classtools/classtools_test.go
 create mode 100644 htmx/ext/ext.go

diff --git a/examples/web/activesearch/extempl/activesearch_templ.go b/examples/web/activesearch/extempl/activesearch_templ.go
index c729e98..c932b74 100644
--- a/examples/web/activesearch/extempl/activesearch_templ.go
+++ b/examples/web/activesearch/extempl/activesearch_templ.go
@@ -13,7 +13,6 @@ 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 0ffd42a..5aa8129 100644
--- a/examples/web/bulkupdate/extempl/bulkupdate_templ.go
+++ b/examples/web/bulkupdate/extempl/bulkupdate_templ.go
@@ -14,7 +14,6 @@ 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/clicktoedit/extempl/clicktoedit_templ.go b/examples/web/clicktoedit/extempl/clicktoedit_templ.go
index 18c9f87..367ea6d 100644
--- a/examples/web/clicktoedit/extempl/clicktoedit_templ.go
+++ b/examples/web/clicktoedit/extempl/clicktoedit_templ.go
@@ -12,7 +12,6 @@ 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/gom/layout/layout.gom.go b/examples/web/layout/gom/layout/layout.gom.go
index 99d92f1..6a4921e 100644
--- a/examples/web/layout/gom/layout/layout.gom.go
+++ b/examples/web/layout/gom/layout/layout.gom.go
@@ -34,6 +34,7 @@ func Wrapper(title string, children ...g.Node) g.Node {
 					hxconfig.New().IncludeIndicatorStyles(false),
 				)),
 				Script(Src("https://unpkg.com/htmx.org@1.9.10")),
+				Script(Src("https://unpkg.com/htmx.org@1.9.10/dist/ext/class-tools.js")),
 				Link(Rel("stylesheet"), Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css")),
 				Link(Rel("stylesheet"), Href("https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/default.min.css")),
 				Link(Rel("stylesheet"), Href("/static/main.css")),
diff --git a/examples/web/layout/templ/layout/layout.templ b/examples/web/layout/templ/layout/layout.templ
index d58eebe..a54a66f 100644
--- a/examples/web/layout/templ/layout/layout.templ
+++ b/examples/web/layout/templ/layout/layout.templ
@@ -31,6 +31,7 @@ templ Wrapper(title string, className ...string) {
 				)... }
 			/>
 			
+			
 			
 		

Demo

- @demo() +
+ @demo() +
} } @@ -79,7 +82,7 @@ templ demo() { { hx.Swap(swap.OuterHTML)... } >

Start Progress

- @@ -89,6 +92,7 @@ templ demo() { templ JobRunning(jobID int64, progress int) { //ex:start:running
Restart Job diff --git a/examples/web/progressbar/extempl/progressbar_templ.go b/examples/web/progressbar/extempl/progressbar_templ.go index 0795f00..ff965ac 100644 --- a/examples/web/progressbar/extempl/progressbar_templ.go +++ b/examples/web/progressbar/extempl/progressbar_templ.go @@ -15,8 +15,8 @@ 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" "github.com/will-wow/typed-htmx-go/htmx/trigger" @@ -58,7 +58,7 @@ func Page() templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "demo")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 35, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 36, Col: 48} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -71,7 +71,7 @@ func Page() templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "running")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 43, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 44, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -84,7 +84,7 @@ func Page() templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "progress")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 48, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 49, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -97,7 +97,7 @@ func Page() templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("progressbar.templ", "done")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 59, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 60, Col: 48} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -110,13 +110,21 @@ func Page() templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(static.ExCSS.PrintOrErr("main.css", "progress-bar-style")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 67, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 68, Col: 63} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Demo

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Demo

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -124,6 +132,10 @@ func Page() templ.Component { 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) } @@ -165,7 +177,7 @@ func demo() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">

Start Progress

") + templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, classtools.Classes(hx, []classtools.Run{{ + classtools.Add("show", time.Millisecond*600), + }})) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(">Restart Job") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -387,7 +409,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: 145, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/progressbar/extempl/progressbar.templ`, Line: 150, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { diff --git a/examples/web/progressbar/progressbar.go b/examples/web/progressbar/progressbar.go index f42e495..4444f71 100644 --- a/examples/web/progressbar/progressbar.go +++ b/examples/web/progressbar/progressbar.go @@ -80,7 +80,7 @@ func (ex *example) progress(w http.ResponseWriter, r *http.Request) { if ex.gom { res.MustWrite(w) - _ = exgom.Page().Render(w) + _ = exgom.ProgressBar(progress).Render(w) } else { res.MustRenderTempl(r.Context(), w, extempl.ProgressBar(progress)) } @@ -106,7 +106,7 @@ func (ex *example) job(w http.ResponseWriter, r *http.Request) { } if ex.gom { - _ = exgom.Page().Render(w) + _ = exgom.Job(id, progress).Render(w) } else { _ = extempl.Job(id, progress).Render(r.Context(), w) } diff --git a/examples/web/static/main.css b/examples/web/static/main.css index dcab66c..7ffa1fd 100644 --- a/examples/web/static/main.css +++ b/examples/web/static/main.css @@ -45,3 +45,10 @@ 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; +} diff --git a/htmx/ext/classtools/classtools.go b/htmx/ext/classtools/classtools.go new file mode 100644 index 0000000..feb26f7 --- /dev/null +++ b/htmx/ext/classtools/classtools.go @@ -0,0 +1,75 @@ +package classtools + +import ( + "strings" + "time" + + "github.com/will-wow/typed-htmx-go/htmx" +) + +const Extension htmx.Extension = "class-tools" + +type operation string + +const ( + operationAdd operation = "add" + operationRemove operation = "remove" + operationToggle operation = "toggle" +) + +type classOperation struct { + Operation operation + Class string + Delay time.Duration +} + +type Run []classOperation + +func Classes[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)) + classes.WriteRune(' ') + classes.WriteString(class.Class) + if class.Delay > 0 { + classes.WriteRune(':') + classes.WriteString(class.Delay.String()) + } + if j < len(run)-1 { + classes.WriteString(", ") + } + } + if i < len(runs)-1 { + classes.WriteString(" & ") + } + } + + return hx.Attr("classes", classes.String()) +} + +func Add(className string, delay ...time.Duration) classOperation { + return makeOperation(operationAdd, className, delay) +} + +func Remove(className string, delay ...time.Duration) classOperation { + return makeOperation(operationRemove, className, delay) +} + +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] + } + + return classOperation{ + Operation: op, + Class: className, + Delay: delayValue, + } +} diff --git a/htmx/ext/classtools/classtools_test.go b/htmx/ext/classtools/classtools_test.go new file mode 100644 index 0000000..36042f9 --- /dev/null +++ b/htmx/ext/classtools/classtools_test.go @@ -0,0 +1,25 @@ +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, []classtools.Run{ + { + classtools.Add("foo"), + classtools.Remove("bar", time.Millisecond*500), + }, + { + classtools.Toggle("baz", time.Second), + }, + }) + fmt.Println(attr) + // Output: classes='add foo, remove bar:500ms & toggle baz:1s' +} diff --git a/htmx/ext/ext.go b/htmx/ext/ext.go new file mode 100644 index 0000000..e95dcae --- /dev/null +++ b/htmx/ext/ext.go @@ -0,0 +1,11 @@ +package ext + +// type Extension string + +// const ( +// ClassTools Extension = "class-tools" +// ) + +func ClassTools(class string) string { + return "class-tools" +} diff --git a/htmx/htmx.go b/htmx/htmx.go index 2d5c0a6..7cad6de 100644 --- a/htmx/htmx.go +++ b/htmx/htmx.go @@ -795,6 +795,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. @@ -809,8 +811,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. @@ -1324,6 +1330,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 From 02b0e5346b9c39effdff49d46f62d3402d54aae2 Mon Sep 17 00:00:00 2001 From: Will Ockelmann-Wagner Date: Mon, 29 Apr 2024 01:28:11 -0700 Subject: [PATCH 7/9] add remove-me and preload extensions --- htmx/ext/preload/preload.go | 34 ++++++++++++++++++++++++++++++ htmx/ext/preload/preload_test.go | 34 ++++++++++++++++++++++++++++++ htmx/ext/removeme/removeme.go | 19 +++++++++++++++++ htmx/ext/removeme/removeme_test.go | 17 +++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 htmx/ext/preload/preload.go create mode 100644 htmx/ext/preload/preload_test.go create mode 100644 htmx/ext/removeme/removeme.go create mode 100644 htmx/ext/removeme/removeme_test.go diff --git a/htmx/ext/preload/preload.go b/htmx/ext/preload/preload.go new file mode 100644 index 0000000..f3e5e50 --- /dev/null +++ b/htmx/ext/preload/preload.go @@ -0,0 +1,34 @@ +package preload + +import "github.com/will-wow/typed-htmx-go/htmx" + +// 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. +const Extension htmx.Extension = "preload" + +func Preload[T any](hx htmx.HX[T]) T { + return hx.Attr("preload", true) +} + +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. +) + +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. +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" +} 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..15c5f64 --- /dev/null +++ b/htmx/ext/removeme/removeme.go @@ -0,0 +1,19 @@ +package removeme + +import ( + "time" + + "github.com/will-wow/typed-htmx-go/htmx" +) + +// Extension allows you to remove an element after a specified interval. +// +// 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. +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' +} From 9943bc152df5d1d09621650dd71059c6019aa302 Mon Sep 17 00:00:00 2001 From: Will Ockelmann-Wagner Date: Fri, 17 May 2024 00:19:41 -0700 Subject: [PATCH 8/9] improve classnames interface, and add an example --- .vscode/settings.json | 5 +- examples/web/classtools_ex/classtools_ex.go | 31 ++++ .../classtools_ex/exgom/classtools_ex.gom.go | 73 ++++++++ .../classtools_ex/extempl/classtools_ex.templ | 72 ++++++++ .../extempl/classtools_ex_templ.go | 159 ++++++++++++++++++ examples/web/examples/exgom/examples.gom.go | 9 +- examples/web/examples/extempl/examples.templ | 5 + .../web/examples/extempl/examples_templ.go | 12 +- .../web/progressbar/exgom/progressbar.gom.go | 4 +- .../web/progressbar/extempl/progressbar.templ | 4 +- .../progressbar/extempl/progressbar_templ.go | 6 +- examples/web/static/main.css | 8 + examples/web/web.go | 2 + htmx/ext/classtools/classtools.go | 78 ++++++--- htmx/ext/classtools/classtools_test.go | 24 ++- htmx/ext/ext.go | 11 -- 16 files changed, 451 insertions(+), 52 deletions(-) create mode 100644 examples/web/classtools_ex/classtools_ex.go create mode 100644 examples/web/classtools_ex/exgom/classtools_ex.gom.go create mode 100644 examples/web/classtools_ex/extempl/classtools_ex.templ create mode 100644 examples/web/classtools_ex/extempl/classtools_ex_templ.go delete mode 100644 htmx/ext/ext.go diff --git a/.vscode/settings.json b/.vscode/settings.json index c9150c7..b9018dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "go.lintTool": "golangci-lint", "go.lintFlags": ["--fast"], - "cSpell.words": ["gomponents"] + "cSpell.words": [ + "classtools", + "gomponents" + ] } diff --git a/examples/web/classtools_ex/classtools_ex.go b/examples/web/classtools_ex/classtools_ex.go new file mode 100644 index 0000000..f4ff40c --- /dev/null +++ b/examples/web/classtools_ex/classtools_ex.go @@ -0,0 +1,31 @@ +package classtools_ex + +import ( + "net/http" + + "github.com/will-wow/typed-htmx-go/examples/web/classtools_ex/exgom" + "github.com/will-wow/typed-htmx-go/examples/web/classtools_ex/extempl" +) + +type example struct { + gom bool +} + +func NewHandler(gom bool) http.Handler { + mux := http.NewServeMux() + + ex := example{gom: gom} + + mux.HandleFunc("GET /{$}", ex.demo) + mux.HandleFunc("GET /foo/{$}", ex.demo) + + 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) + } +} diff --git a/examples/web/classtools_ex/exgom/classtools_ex.gom.go b/examples/web/classtools_ex/exgom/classtools_ex.gom.go new file mode 100644 index 0000000..e162e5f --- /dev/null +++ b/examples/web/classtools_ex/exgom/classtools_ex.gom.go @@ -0,0 +1,73 @@ +package exgom + +import ( + "embed" + "time" + + . "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" + "github.com/will-wow/typed-htmx-go/examples/web/layout/gom/layout" + + "github.com/will-wow/typed-htmx-go/htmx" +) + +var hx = htmx.NewGomponents() + +//go:embed classtools_ex.gom.go +var fs embed.FS +var ex = exprint.New(fs, "//", "") + +func Page() g.Node { + return layout.Wrapper( + "Class Tools", + Class("class-tools-ex"), + H1(g.Text("Class Tools")), + P( + g.Text("Demonstrates different uses of class-tools"), + ), + Pre( + Code( + Class("language-go"), + g.Text(ex.PrintOrErr("classtools_ex.gom.go", "demo")), + ), + ), + H2(g.Text("Demo")), + demo(), + ) +} + +func demo() g.Node { + //ex:start:demo + return Div( + hx.Ext(classtools.Extension), + P(g.Text("Add then remove bold after 1 second, then toggle color every second"), + classtools.Classes(hx, + classtools.Add("bold", time.Second), + classtools.Remove("bold", time.Second), + classtools.Toggle("color", time.Second), + ), + ), + P(g.Text("Add then remove bold after 1 second, while toggling color every second"), + classtools.ClassesParallel(hx, []classtools.Run{ + { + classtools.Add("bold", time.Second), + classtools.Remove("bold", time.Second), + }, + { + classtools.Toggle("color", time.Second), + }, + }), + ), + P(g.Text("Add with no delay"), + classtools.Classes(hx, classtools.Add("color", 0)), + ), + P(g.Text("Toggle with 0 delay"), + classtools.Classes(hx, classtools.Toggle("color", 0)), + ), + ) + //ex:end:demo +} diff --git a/examples/web/classtools_ex/extempl/classtools_ex.templ b/examples/web/classtools_ex/extempl/classtools_ex.templ new file mode 100644 index 0000000..6e1ae32 --- /dev/null +++ b/examples/web/classtools_ex/extempl/classtools_ex.templ @@ -0,0 +1,72 @@ +package extempl + +import ( + "embed" + "time" + + "github.com/will-wow/typed-htmx-go/examples/web/layout/templ/layout" + "github.com/will-wow/typed-htmx-go/examples/web/exprint" + "github.com/will-wow/typed-htmx-go/htmx/ext/classtools" + "github.com/will-wow/typed-htmx-go/htmx" +) + +var hx = htmx.NewTempl() + +//go:embed classtools_ex.templ +var fs embed.FS +var ex = exprint.New(fs, "//", "") + +templ Page() { + @layout.Wrapper("Class Tools", "class-tools-ex") { +

Class Tools

+

+ Demonstrates different uses of class-tools +

+
+			
+				{ ex.PrintOrErr("classtools_ex.templ", "demo") }
+			
+		
+

Demo

+ @demo() + } +} + +templ demo() { + //ex:start:demo +
+

+ Add then remove bold after 1 second, then toggle color every second +

+

+ Add then remove bold after 1 second, while toggling color every second +

+

+ Add with no delay +

+

+ Toggle with 0 delay +

+
+ //ex:end:demo +} diff --git a/examples/web/classtools_ex/extempl/classtools_ex_templ.go b/examples/web/classtools_ex/extempl/classtools_ex_templ.go new file mode 100644 index 0000000..a9f24b9 --- /dev/null +++ b/examples/web/classtools_ex/extempl/classtools_ex_templ.go @@ -0,0 +1,159 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.663 +package extempl + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import ( + "bytes" + "context" + "embed" + "io" + "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/examples/web/exprint" + "github.com/will-wow/typed-htmx-go/examples/web/layout/templ/layout" +) + +var hx = htmx.NewTempl() + +//go:embed classtools_ex.templ +var fs embed.FS +var ex = exprint.New(fs, "//", "") + +func 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("

Class Tools

Demonstrates different uses of class-tools

")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var3 string
+			templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(ex.PrintOrErr("classtools_ex.templ", "demo"))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/classtools_ex/extempl/classtools_ex.templ`, Line: 27, Col: 50}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Demo

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = demo().Render(ctx, templ_7745c5c3_Buffer) + 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 = layout.Wrapper("Class Tools", "class-tools-ex").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 demo() 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_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("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) { 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 @@ -coverage: 99.3%coverage99.3% \ No newline at end of file +coverage: 99.4%coverage99.4% \ 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" -}