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/.vscode/settings.json b/.vscode/settings.json index c9150c7..2fbef1d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { "go.lintTool": "golangci-lint", "go.lintFlags": ["--fast"], - "cSpell.words": ["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/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 01c456b..dc8e48e 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -5,13 +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 7d502e5..374c833 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,46 +1,52 @@ -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= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= 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/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..3413f06 --- /dev/null +++ b/examples/web/classtools_ex/exgom/classtools_ex.gom.go @@ -0,0 +1,74 @@ +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..22e95e5 --- /dev/null +++ b/examples/web/classtools_ex/extempl/classtools_ex_templ.go @@ -0,0 +1,160 @@ +// 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 e3b447d..2e4ab3b 100644 --- a/examples/web/examples/exgom/examples.gom.go +++ b/examples/web/examples/exgom/examples.gom.go @@ -40,10 +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/", + "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 728065e..77eb722 100644 --- a/examples/web/examples/extempl/examples.templ +++ b/examples/web/examples/extempl/examples.templ @@ -33,20 +33,30 @@ 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", + ) + @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 48b01d0..cfa2cd8 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,29 @@ 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 = 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 @@ -111,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: 58, 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 { @@ -124,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: 61, 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/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) { )... } /> + 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", "demo") }
+			
+		
+

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

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

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

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

Demo

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

Start Progress

+ +
+ //ex:end:demo +} + +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 diff --git a/examples/web/progressbar/extempl/progressbar_templ.go b/examples/web/progressbar/extempl/progressbar_templ.go new file mode 100644 index 0000000..6d6ffd5 --- /dev/null +++ b/examples/web/progressbar/extempl/progressbar_templ.go @@ -0,0 +1,442 @@ +// 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/ext/classtools" + "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" + "github.com/will-wow/typed-htmx-go/examples/web/static" +) + +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", "demo"))
+			if templ_7745c5c3_Err != nil {
+				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 {
+				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: 44, 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: 49, 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 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: 60, 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(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: 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

") + 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 + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer) + } + return templ_7745c5c3_Err + }) + templ_7745c5c3_Err = 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 + } + 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_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

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