diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e380ce..c9150c7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "go.lintTool": "golangci-lint", - "go.lintFlags": ["--fast"] + "go.lintFlags": ["--fast"], + "cSpell.words": ["gomponents"] } diff --git a/README.md b/README.md index 2e2b0b9..6a471da 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,21 @@ Well-documented Go functions for building [HTMX](https://htmx.org) attributes. [![Go Reference](https://pkg.go.dev/badge/github.com/will-wow/typed-htmx-go.svg)](https://pkg.go.dev/github.com/will-wow/typed-htmx-go/hx) ![Code Coverage](./assets/badge.svg) -[htmx](https://htmx.org) is a powerful and simple tool for building dynamic, server-rendered web applications. It also pairs particularly well with [Templ](https://templ.guide), a JSX-like template builder for Go. +[htmx](https://htmx.org) is a powerful and simple tool for building dynamic, server-rendered web applications. It also pairs particularly well with [Templ](https://templ.guide) (a JSX-like template language for Go) and [Gomponents](https://www.gomponents.com/) (a Go-native view library). However, when using it I have to have the [docs](https://htmx.org/reference) open, to look up the specifics of each modifier. I wanted the simplicity of HTMX, the editor support of Go, and beautiful integration with Templ, without sacrificing performance. I built typed-htmx-go. -`hx.New()` provides a builder struct that wraps all documented [HTMX attributes](https://htmx.org/reference/) with Go functions, and `.Build()` returns a map that conforms to [templ.Attributes](https://templ.guide/syntax-and-usage/attributes). This allows the result to be spread into a Templ element or be passed to a Templ component. However this library has no actual dependency of Templ, and can be used by anything that can render a `map[string]any` to HTML attributes. You can also use `.String()` to get a formatted string of HTML attributes to directly render in a template. +`hx.NewTempl()` provides an `hx` struct that exposes all documented [HTMX attributes](https://htmx.org/reference/) as Go functions, and returns either [templ.Attributes](https://templ.guide/syntax-and-usage/attributes) for to be spread into a Templ element, or an attribute `g.Node` for Gomponents. You can also support other templating libraries by simply passing a new attribute constructor to `HX{}`. Each function and option includes a Godoc comment copied from the extensive HTMX docs, so you can access that documentation right from the comfort of your editor. ## Examples -Templ usage examples are in [examples/templ](./examples/templ) (hosted at [typed-htmx-go.vercel.app](https://typed-htmx-go.vercel.app/)) +Usage examples are in [examples](./examples) (hosted at [typed-htmx-go.vercel.app](https://typed-htmx-go.vercel.app/)) ## HTMX Version -`hx` strives to keep up with HTMX releases. It currently supports HTMX `v1.9.10`. +`typed-hx-go` strives to keep up with HTMX releases. It currently supports HTMX `v1.9.10`. ## Goals @@ -39,9 +39,9 @@ That's necessary for a tool that embeds in standard HTML attributes, but it requ Sometimes that means that `hx` will provide multiple functions for a single attribute. For instance, `hx` provides three methods for `hx-target`, stop you from doing `hx-target='this #element'` (which is invalid), and instead guide you towards valid options like: -- `.Target("#element")` => `hx-target="#element'` -- `.TargetNonStandard(hx.TargetThis)` => `hx-target='this'` -- `.TargetRelative(hx.TargetSelectorNext, "#element")` => `hx-target='next #element'` +- `hx.Target("#element")` => `hx-target="#element'` +- `hx.TargetNonStandard(hx.TargetThis)` => `hx-target='this'` +- `hx.TargetRelative(hx.TargetSelectorNext, "#element")` => `hx-target='next #element'` As a corollary to this goal, it should also be difficult to create an invalid attribute. So if modifier must be accompanied by a selector (like the `next` in `hx-target`), then it must be exposed through a two-argument function. @@ -73,29 +73,36 @@ This also means that written `hx` attributes should look like HTMX attributes. S The `hx` equivalent should take the same names and values in the same order: ```go +// templ
``` -### Templ compatibility - -While this library isn't tied to Templ directly, it should always return attribute maps that work as [Templ attributes](https://templ.guide/syntax-and-usage/attributes) for spreading, and generally work nicely within Templ components. - -However, it should also be possible to directly print attributes for use in a standard Go [html/template](https://pkg.go.dev/html/template#HTMLAttrhttps://pkg.go.dev/html/template) (with [HTMLAttr](https://pkg.go.dev/html/template#HTMLAttrhttps://pkg.go.dev/html/template#HTMLAttr)). - -TODO: Figure out a safer method of including in an html/template. +```go +// gomponents +Form( + Method("GET"), + Action("/page"), + hx.Get("/page"), + hx.Target("body"), + hx.ReplaceURL(true), + hx.Swap( + swap.New(). + ScrollElement("#search-results", swap.Top). + Swap(time.Second), + ), +) +``` ### Fully tested @@ -111,34 +118,34 @@ go get github.com/will-wow/typed-htmx-go ```go import ( - "github.com/will-wow/typed-htmx-go/hx" - "github.com/will-wow/typed-htmx-go/hx/swap" + "github.com/will-wow/typed-htmx-go/htmx" + "github.com/will-wow/typed-htmx-go/htmx/swap" ) +var hx = htmx.NewTempl() + templ SearchInput(search string) { @ui.Input( - hx.New(). - Get(currentPage). - Trigger("input changed delay:500ms"). - More(map[string]any{ + htmx.TemplAttrs( + hx.Get(currentPage), + hx.Trigger("input changed delay:500ms"), + templ.Attributes{ "id": "search", "placeholder": "Search (/)", "type": "search", "name": "search", "value": search, - }). - Build(), + }, + ), "peer")
@icon.Search() diff --git a/Taskfile.yml b/Taskfile.yml index 9fa9159..d282a07 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -55,6 +55,7 @@ tasks: fmt:go: desc: Run goimports cmds: + - go work sync - go mod tidy - go run golang.org/x/tools/cmd/goimports@v0.18.0 -w -local github.com/will-wow/typed-htmx-go . @@ -69,9 +70,20 @@ tasks: - task: fmt - task: lint - task: test:cover:badge + - task: examples:ready tools: desc: Install tools cmds: # Install prettier for formatting non-go files - npm install + + dev: + desc: Run the example server with a watch for templ, go, and static files + cmds: + - task: examples:dev + +includes: + examples: + taskfile: ./examples/Taskfile.yml + dir: ./examples diff --git a/assets/badge.svg b/assets/badge.svg index e1ae2d7..50f04ba 100644 --- a/assets/badge.svg +++ b/assets/badge.svg @@ -1 +1 @@ -coverage: 81.2%coverage81.2% \ No newline at end of file +coverage: 76.5%coverage76.5% \ No newline at end of file diff --git a/examples/templ/.gitignore b/examples/.gitignore similarity index 100% rename from examples/templ/.gitignore rename to examples/.gitignore diff --git a/examples/templ/.vscode/examples.code-snippets b/examples/.vscode/examples.code-snippets similarity index 92% rename from examples/templ/.vscode/examples.code-snippets rename to examples/.vscode/examples.code-snippets index 64ffb1a..f1a7ef4 100644 --- a/examples/templ/.vscode/examples.code-snippets +++ b/examples/.vscode/examples.code-snippets @@ -40,9 +40,9 @@ "import (", " \"github.com/lithammer/dedent\"", "", - " \"github.com/will-wow/typed-htmx-go/examples/templ/web/layout\"", + " \"github.com/will-wow/typed-htmx-go/examples/web/layout\"", " \"github.com/will-wow/typed-htmx-go/hx\"", - " \"github.com/will-wow/typed-htmx-go/hx/swap\"", + " \"github.com/will-wow/typed-htmx-go/htmx/swap\"", ")", "", "templ page() {", diff --git a/examples/templ/Taskfile.yml b/examples/Taskfile.yml similarity index 91% rename from examples/templ/Taskfile.yml rename to examples/Taskfile.yml index f99429a..0e3131d 100644 --- a/examples/templ/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/templ . + # - go mod tidy + - go run golang.org/x/tools/cmd/goimports@v0.18.0 -w -local github.com/will-wow/typed-htmx-go/examples . test: desc: Run test suite @@ -44,7 +44,7 @@ tasks: cmds: - templ generate # Re-format generated code - - go run golang.org/x/tools/cmd/goimports@v0.18.0 -w -local github.com/will-wow/typed-htmx-go/examples/templ ./ + - go run golang.org/x/tools/cmd/goimports@v0.18.0 -w -local github.com/will-wow/typed-htmx-go/examples ./ sources: - "**/*.templ" generates: diff --git a/examples/templ/api/README.md b/examples/api/README.md similarity index 100% rename from examples/templ/api/README.md rename to examples/api/README.md diff --git a/examples/templ/api/index.go b/examples/api/index.go similarity index 72% rename from examples/templ/api/index.go rename to examples/api/index.go index 79bf77a..40e4084 100644 --- a/examples/templ/api/index.go +++ b/examples/api/index.go @@ -3,7 +3,7 @@ package api import ( "net/http" - "github.com/will-wow/typed-htmx-go/examples/templ/web" + "github.com/will-wow/typed-htmx-go/examples/web" ) func Index(w http.ResponseWriter, r *http.Request) { diff --git a/examples/checksum/gen b/examples/checksum/gen new file mode 100644 index 0000000..94f4e51 --- /dev/null +++ b/examples/checksum/gen @@ -0,0 +1 @@ +234b8f866e8559473698c77116b5f561 diff --git a/examples/templ/cmd/server/main.go b/examples/cmd/server/main.go similarity index 87% rename from examples/templ/cmd/server/main.go rename to examples/cmd/server/main.go index a1f7fc6..0b376e4 100644 --- a/examples/templ/cmd/server/main.go +++ b/examples/cmd/server/main.go @@ -6,7 +6,7 @@ import ( "os" "time" - "github.com/will-wow/typed-htmx-go/examples/templ/web" + "github.com/will-wow/typed-htmx-go/examples/web" ) func main() { diff --git a/examples/templ/go.mod b/examples/go.mod similarity index 59% rename from examples/templ/go.mod rename to examples/go.mod index 05bf2d0..5edda6a 100644 --- a/examples/templ/go.mod +++ b/examples/go.mod @@ -1,15 +1,13 @@ -module github.com/will-wow/typed-htmx-go/examples/templ +module github.com/will-wow/typed-htmx-go/examples go 1.22.0 -require ( - github.com/a-h/templ v0.2.543 - github.com/will-wow/typed-htmx-go v0.0.3 -) +require github.com/a-h/templ v0.2.543 require ( github.com/PuerkitoBio/goquery v1.8.1 github.com/lithammer/dedent v1.1.0 + github.com/maragudk/gomponents v0.20.2 ) require ( diff --git a/examples/templ/go.sum b/examples/go.sum similarity index 95% rename from examples/templ/go.sum rename to examples/go.sum index 856945a..7faa85d 100644 --- a/examples/templ/go.sum +++ b/examples/go.sum @@ -8,8 +8,8 @@ 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/will-wow/typed-htmx-go v0.0.3 h1:zF0ESFMm2Ry2OEe+mQ+2zcULM1aEqWaDwdyGEZWC/1w= -github.com/will-wow/typed-htmx-go v0.0.3/go.mod h1:vBV9acu4/cjaeLc1Z2z4ZruJYpF1KdCoFebp3CWNaPA= +github.com/maragudk/gomponents v0.20.2 h1:39FhnBNNCJzqNcD9Hmvp/5xj0otweFoyvVgFG6kXoy0= +github.com/maragudk/gomponents v0.20.2/go.mod h1:nHkNnZL6ODgMBeJhrZjkMHVvNdoYsfmpKB2/hjdQ0Hg= 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= diff --git a/examples/templ/web/bulkupdate/bulkupdate.go b/examples/templ/web/bulkupdate/bulkupdate.go deleted file mode 100644 index b7b142d..0000000 --- a/examples/templ/web/bulkupdate/bulkupdate.go +++ /dev/null @@ -1,65 +0,0 @@ -package bulkupdate - -import ( - "fmt" - "net/http" -) - -func Handler() http.Handler { - mux := http.NewServeMux() - - mux.HandleFunc("GET /", demo) - mux.HandleFunc("POST /", post) - - return mux -} - -type userModel struct { - name string - email string - active bool -} - -func demo(w http.ResponseWriter, r *http.Request) { - users := defaultUsers() - - component := page(users) - w.WriteHeader(http.StatusOK) - _ = component.Render(r.Context(), w) -} - -func post(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - users := defaultUsers() - - var additions int - var removals int - - for _, user := range users { - if r.Form.Has(user.email) && !user.active { - additions++ - } else if !r.Form.Has(user.email) && user.active { - removals++ - } - } - - toast := fmt.Sprintf("Activated %d and deactivated %d users", additions, removals) - - component := updateToast(toast) - w.WriteHeader(http.StatusOK) - _ = component.Render(r.Context(), w) -} - -func defaultUsers() []userModel { - return []userModel{ - {name: "Joe Smith", email: "joe@smith.org", active: true}, - {name: "Angie MacDowell", email: "angie@macdowell.org", active: true}, - {name: "Fuqua Tarkenton", email: "fuqua@tarkenton.org", active: true}, - {name: "Kim Yee", email: "kim@yee.org", active: false}, - } -} diff --git a/examples/templ/web/clicktoedit/clicktoedit.go b/examples/templ/web/clicktoedit/clicktoedit.go deleted file mode 100644 index 297d571..0000000 --- a/examples/templ/web/clicktoedit/clicktoedit.go +++ /dev/null @@ -1,109 +0,0 @@ -package clicktoedit - -import ( - "net/http" - "strings" - - "github.com/will-wow/typed-htmx-go/examples/templ/web/ui" -) - -type form struct { - ui.Form - FirstName string - LastName string - Email string -} - -func (f *form) validate() (ok bool) { - if f.FirstName == "" { - f.SetRequiredError("FirstName") - } - if f.LastName == "" { - f.SetRequiredError("LastName") - } - if f.Email == "" { - f.SetRequiredError("Email") - } else if !strings.Contains(f.Email, "@") { - f.SetError("Email", "Invalid email address") - } - - return !f.HasErrors() -} - -func newForm() *form { - return &form{ - FirstName: "", - LastName: "", - Email: "", - Form: ui.NewForm(), - } -} - -func Handler() http.Handler { - mux := http.NewServeMux() - - mux.HandleFunc("GET /{$}", demo) - mux.HandleFunc("GET /view", view) - mux.HandleFunc("GET /edit", edit) - mux.HandleFunc("POST /edit", post) - - return mux -} - -func demo(w http.ResponseWriter, r *http.Request) { - form := newForm() - - component := page(form) - - w.WriteHeader(http.StatusOK) - _ = component.Render(r.Context(), w) -} - -func view(w http.ResponseWriter, r *http.Request) { - form := newForm() - - component := viewForm(form) - w.WriteHeader(http.StatusOK) - _ = component.Render(r.Context(), w) -} - -func edit(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - form := newForm() - - component := editForm(form) - w.WriteHeader(http.StatusOK) - _ = component.Render(r.Context(), w) -} - -func post(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - form := &form{ - FirstName: r.Form.Get("firstName"), - LastName: r.Form.Get("lastName"), - Email: r.Form.Get("email"), - Form: ui.NewForm(), - } - ok := form.validate() - - if !ok { - component := editForm(form) - w.WriteHeader(http.StatusUnprocessableEntity) - _ = component.Render(r.Context(), w) - return - } - - component := viewForm(form) - w.WriteHeader(http.StatusOK) - _ = component.Render(r.Context(), w) -} diff --git a/examples/templ/web/error.templ b/examples/templ/web/error.templ deleted file mode 100644 index af4761d..0000000 --- a/examples/templ/web/error.templ +++ /dev/null @@ -1,19 +0,0 @@ -package web - -import ( - "github.com/will-wow/typed-htmx-go/examples/templ/web/layout" -) - -templ notFoundPage() { - @layout.Base("Not Found") { -

Not Found

-

The page you are looking for does not exist.

- } -} - -templ serverErrorPage(err string) { - @layout.Base("Server Error") { -

Something went wrong

-

{ err }

- } -} diff --git a/examples/templ/web/error_templ.go b/examples/templ/web/error_templ.go deleted file mode 100644 index 0544bc0..0000000 --- a/examples/templ/web/error_templ.go +++ /dev/null @@ -1,107 +0,0 @@ -// Code generated by templ - DO NOT EDIT. - -// templ: version: v0.2.543 -package web - -//lint:file-ignore SA4006 This context is only used if a nested component is present. - -import ( - "bytes" - "context" - "io" - - "github.com/a-h/templ" - - "github.com/will-wow/typed-htmx-go/examples/templ/web/layout" -) - -func notFoundPage() templ.Component { - return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_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("

Not Found

The page you are looking for does not exist.

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer) - } - return templ_7745c5c3_Err - }) - templ_7745c5c3_Err = layout.Base("Not Found").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 serverErrorPage(err string) templ.Component { - return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var3 := templ.GetChildren(ctx) - if templ_7745c5c3_Var3 == nil { - templ_7745c5c3_Var3 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var4 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Something went wrong

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(err) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/error.templ`, Line: 16, Col: 10} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer) - } - return templ_7745c5c3_Err - }) - templ_7745c5c3_Err = layout.Base("Server Error").Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) - } - return templ_7745c5c3_Err - }) -} diff --git a/examples/templ/web/examples/examples.go b/examples/templ/web/examples/examples.go deleted file mode 100644 index e999703..0000000 --- a/examples/templ/web/examples/examples.go +++ /dev/null @@ -1,12 +0,0 @@ -package examples - -import ( - "net/http" -) - -func Handler(w http.ResponseWriter, r *http.Request) { - component := page() - w.WriteHeader(http.StatusOK) - - _ = component.Render(r.Context(), w) -} diff --git a/examples/templ/web/examples/examples_templ.go b/examples/templ/web/examples/examples_templ.go deleted file mode 100644 index 9e49b0e..0000000 --- a/examples/templ/web/examples/examples_templ.go +++ /dev/null @@ -1,134 +0,0 @@ -// Code generated by templ - DO NOT EDIT. - -// templ: version: v0.2.543 -package examples - -//lint:file-ignore SA4006 This context is only used if a nested component is present. - -import ( - "bytes" - "context" - "io" - - "github.com/a-h/templ" - - "github.com/will-wow/typed-htmx-go/examples/templ/web/layout" -) - -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("

UI Examples

Below are a set of UX patterns implemented in htmx with minimal HTML and styling.

These are ported from the htmx examples and are intended showcase the use of hx when building HTMX applications.

You can copy and paste them and then adjust them for your needs.

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = exampleRow( - "/examples/click-to-edit", - "Click To Edit", - "Demonstrates inline editing of a data object", - ).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = exampleRow( - "/examples/bulk-update", - "Bulk Update", - "Demonstrates bulk updating of multiple rows of data", - ).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
PatternDescription
") - 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.Base("").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) - } - return templ_7745c5c3_Err - }) -} - -func exampleRow(link, name, description string) templ.Component { - return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var3 := templ.GetChildren(ctx) - if templ_7745c5c3_Var3 == nil { - templ_7745c5c3_Var3 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/examples.templ`, Line: 52, Col: 41} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(description) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `web/examples/examples.templ`, Line: 55, Col: 16} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) - } - return templ_7745c5c3_Err - }) -} diff --git a/examples/templ/vercel.json b/examples/vercel.json similarity index 100% rename from examples/templ/vercel.json rename to examples/vercel.json diff --git a/examples/templ/web/bulkupdate/README.md b/examples/web/bulkupdate/README.md similarity index 100% rename from examples/templ/web/bulkupdate/README.md rename to examples/web/bulkupdate/README.md diff --git a/examples/web/bulkupdate/bulkupdate.go b/examples/web/bulkupdate/bulkupdate.go new file mode 100644 index 0000000..f91236c --- /dev/null +++ b/examples/web/bulkupdate/bulkupdate.go @@ -0,0 +1,80 @@ +package bulkupdate + +import ( + "fmt" + "net/http" +) + +func NewHandler(gom bool) http.Handler { + mux := http.NewServeMux() + + ex := example{gom: gom} + + mux.HandleFunc("GET /", ex.demo) + mux.HandleFunc("POST /", ex.post) + + return mux +} + +type userModel struct { + Name string + Email string + Active bool +} + +var tEx = newTemplExample() +var gEx = newGomExample() + +type example struct { + gom bool +} + +func (e example) demo(w http.ResponseWriter, r *http.Request) { + users := defaultUsers() + + if e.gom { + _ = gEx.page(users).Render(w) + } else { + component := tEx.page(users) + _ = component.Render(r.Context(), w) + } +} + +func (e example) post(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + users := defaultUsers() + + var additions int + var removals int + + for _, user := range users { + if r.Form.Has(user.Email) && !user.Active { + additions++ + } else if !r.Form.Has(user.Email) && user.Active { + removals++ + } + } + + toast := fmt.Sprintf("Activated %d and deactivated %d users", additions, removals) + + if e.gom { + _ = gEx.updateToast(toast).Render(w) + } else { + component := tEx.updateToast(toast) + _ = component.Render(r.Context(), w) + } +} + +func defaultUsers() []userModel { + return []userModel{ + {Name: "Joe Smith", Email: "joe@smith.org", Active: true}, + {Name: "Angie MacDowell", Email: "angie@macdowell.org", Active: true}, + {Name: "Fuqua Tarkenton", Email: "fuqua@tarkenton.org", Active: true}, + {Name: "Kim Yee", Email: "kim@yee.org", Active: false}, + } +} diff --git a/examples/web/bulkupdate/bulkupdate.gom.go b/examples/web/bulkupdate/bulkupdate.gom.go new file mode 100644 index 0000000..5650a45 --- /dev/null +++ b/examples/web/bulkupdate/bulkupdate.gom.go @@ -0,0 +1,99 @@ +package bulkupdate + +import ( + "time" + + g "github.com/maragudk/gomponents" + . "github.com/maragudk/gomponents/html" + + "github.com/will-wow/typed-htmx-go/htmx" + "github.com/will-wow/typed-htmx-go/htmx/swap" + + "github.com/will-wow/typed-htmx-go/examples/web/layout" +) + +var ghx = htmx.NewGomponents() + +type gomExample struct { + layout layout.Gomponents +} + +func newGomExample() gomExample { + return gomExample{ + layout: layout.Gomponents{}, + } +} + +func (e gomExample) page(users []userModel) g.Node { + return e.layout.Base( + "Bulk Update", + Class("bulk-update"), + H1(g.Text("Bulk Update")), + P( + g.Text("This demo shows how to implement a common pattern where rows are selected and then bulk updated. This is accomplished by putting a form around a table, with checkboxes in the table, and then including the checked values in the form submission"), + Code(g.Text("(POST request)")), + g.Text(":"), + ), + e.table(users), + ) +} + +func (e gomExample) table(users []userModel) g.Node { + return FormEl( + ID("checked-contacts"), + ghx.Post("/examples/gomponents/bulk-update/"), + ghx.SwapExtended( + swap.New().Strategy(swap.OuterHTML).Settle(3*time.Second), + ), + ghx.Target( + "#toast", + ), + + H3(g.Text("Select Rows And Activate Or Deactivate Below")), + Table( + THead( + Tr( + Td(g.Text("Name")), + Td(g.Text("Email")), + Td(g.Text("Activate")), + ), + ), + TBody( + ID("tbody"), + g.Group( + g.Map(users, func(u userModel) g.Node { + return Tr( + Td(g.Text(u.Name)), + Td(g.Text(u.Email)), + Td( + Input( + Type("checkbox"), + Name(u.Email), + g.If(u.Active, + Checked(), + ), + ), + ), + ) + + }), + ), + ), + ), + Input( + Type("submit"), + Value("Submit"), + ), + e.updateToast(""), + ) +} + +func (e gomExample) updateToast(toast string) g.Node { + return Span( + ID("toast"), + g.If(toast != "", + g.Attr("aria-live", "polite"), + ), + g.Text(toast), + ) +} diff --git a/examples/templ/web/bulkupdate/bulkupdate.templ b/examples/web/bulkupdate/bulkupdate.templ similarity index 80% rename from examples/templ/web/bulkupdate/bulkupdate.templ rename to examples/web/bulkupdate/bulkupdate.templ index 5cb4c37..2a92c4e 100644 --- a/examples/templ/web/bulkupdate/bulkupdate.templ +++ b/examples/web/bulkupdate/bulkupdate.templ @@ -5,13 +5,25 @@ import ( "github.com/lithammer/dedent" - "github.com/will-wow/typed-htmx-go/examples/templ/web/layout" - "github.com/will-wow/typed-htmx-go/hx" - "github.com/will-wow/typed-htmx-go/hx/swap" + "github.com/will-wow/typed-htmx-go/examples/web/layout" + "github.com/will-wow/typed-htmx-go/htmx" + "github.com/will-wow/typed-htmx-go/htmx/swap" ) -templ page(users []userModel) { - @layout.Base("Bulk Update", "bulk-update") { +var thx = htmx.NewTempl() + +type templExample struct { + layout layout.Templ +} + +func newTemplExample() templExample { + return templExample{ + layout: layout.Templ{}, + } +} + +templ (e *templExample) page(users []userModel) { + @e.layout.Base("Bulk Update", "bulk-update") {

Bulk Update

This demo shows how to implement a common pattern where rows are selected and then bulk updated. This is accomplished by putting a form around a table, with checkboxes in the table, and then including the checked values in the form submission (POST request): @@ -108,20 +120,18 @@ templ page(users []userModel) {

Demo

- @table(users) + @e.table(users) } } -templ table(users []userModel) { +templ (e *templExample) table(users []userModel) {

Select Rows And Activate Or Deactivate Below

@@ -135,13 +145,13 @@ templ table(users []userModel) { for _, user := range users { - - + +
{ user.name }{ user.email }{ user.Name }{ user.Email } @@ -151,11 +161,11 @@ templ table(users []userModel) {
- @updateToast("") + @e.updateToast("") } -templ updateToast(toast string) { +templ (e *templExample) updateToast(toast string) { Click To Edit

The click to edit pattern provides a way to offer inline editing of all or part of a record without a page refresh. @@ -105,7 +117,7 @@ templ page(form *form) { Cancel @@ -132,12 +144,15 @@ templ page(form *form) {

Demo

- @viewForm(form) + @e.viewForm(form) } } -templ viewForm(form *form) { -
+templ (e templExample) viewForm(form *form) { +
First Name
{ cmp.Or(form.FirstName, "None") }
@@ -146,7 +161,7 @@ templ viewForm(form *form) {
Email
{ cmp.Or(form.Email, "None") }
-
@@ -154,15 +169,13 @@ templ viewForm(form *form) {
} -templ editForm(form *form) { +templ (e templExample) editForm(form *form) {