diff --git a/README.md b/README.md index 801c4c4..30d789e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# typed-htmx-go +# typed-htmx-go/hx Well-documented Go functions for building [HTMX](https://htmx.org) attributes. @@ -9,10 +9,90 @@ Well-documented Go functions for building [HTMX](https://htmx.org) attributes. However, when using it I constantly 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. -typed-htmx-go 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` 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. 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. +## Goals + +The project has some specific goals that drive the API. + +### Complete HTMX attribute support + +Every documented HTMX attribute and modifier should have a corresponding Go function. If we're missing something please submit an issue or a PR! And in the meantime, you can always drop back to a raw HTML attribute. + +### No stringly-typed options + +Many HTMX attributes (like hx-swap and hx-trigger) support a complex syntax of methods, modifiers, and selectors in the attribute string (like `hx-trigger='click[isActive] consume from:(#parent > #child) queue:first target:#element'`). + +That's necessary for a tool that embeds in standard HTML attributes, but it requires a lot of studying the docs to get exactly right. + +`hx` strives to provide function signatures and typed options that ensure you're passing the right options to the right modifiers. + +Sometimes that means that `hx` will provide multiple functions for a single attribute. For instance, `hx-target` has three methods to stop you from doing `hx-target='this #element'` (which is invalid), and instead guide you towards valid options like: + +- `hx-target="#element'` (`.Target("#element")`) +- `hx-target='this'` (`.TargetNonStandard(hx.TargetThis)`) +- `hx-target='next #element'` (`.TargetRelative(hx.TargetSelectorNext, "#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. + +### Full documentation in-editor + +The [HTMX References](https://htmx.org/reference/) are through and readable (otherwise this project wouldn't have been possible!) However, having those great docs at your fingertips as you write, instead of in a separate tab, is even better. + +`hx` strives to have a Go-adjusted copy of every line of documentation from the HTMX References, including examples, included in the godocs of functions and options. + +Note: This work is on going. If you see something missing, please submit a PR! + +### Transferable HTMX skills + +As much as possible, it should be the case that if you know HTMX, you can use `hx`, and using `hx` should prepare you to use raw HTMX. That means that attributes functions should match their true HTMX counterparts, arguments should match names in the docs, and arguments should occur in the order they are printed in the HTML. + +This also means that written `hx` attributes should look like HTMX attributes. So if in HTMX you would write: + +```html +
+``` + +The `hx` equivalent should take the same names and values in the same order: + +```go +
+``` + +### 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. + +### Fully tested + +Every attribute and should have a test to make sure it's printing valid HTMX. These are also a good opportunity to try out the API and make sure it's ergonomic in practice. + ## Install ```bash @@ -30,7 +110,7 @@ import ( templ SearchInput(search string) { coverage: 57.5%coverage57.5% \ No newline at end of file +coverage: 60.2%coverage60.2% \ No newline at end of file diff --git a/hx/hx.go b/hx/hx.go index d9f371b..2e0ab28 100644 --- a/hx/hx.go +++ b/hx/hx.go @@ -734,6 +734,31 @@ func (hx HX) ValsJS(vals map[string]string) HX { // Additional Attributes +// Confirm allows you to confirm an action before issuing a request. This can be useful in cases where the action is destructive and you want to ensure that the user really wants to do it. +// +// Here is an example: +// +// +// +// # Event details +// +// The event triggered by hx-confirm contains additional properties in its detail: +// +// - triggeringEvent: the event that triggered the original request +// - issueRequest(skipConfirmation=false): a callback which can be used to confirm the AJAX request +// - question: the value of the hx-confirm attribute on the HTML element +// +// # Notes +// +// - hx-confirm is inherited and can be placed on a parent element +// - hx-confirm uses the browser’s window.confirm by default. You can customize this behavior as shown in this example. +// - a boolean skipConfirmation can be passed to the issueRequest callback; if true (defaults to false), the window.confirm will not be called and the AJAX request is issued directly +// +// HTMX Attribute: [hx-confirm] +// +// [hx-confirm]: https://htmx.org/attributes/hx-confirm/ func (hx HX) Confirm(msg string) HX { hx.attrs["hx-confirm"] = msg return hx diff --git a/hx/hx_test.go b/hx/hx_test.go index 5c98d00..fe294d0 100644 --- a/hx/hx_test.go +++ b/hx/hx_test.go @@ -80,7 +80,7 @@ func TestHX(t *testing.T) { attrs: hx.New().SwapExtended( swap.New(). Strategy(swap.OuterHTML). - SettleTiming("1s"). + Settle(time.Second). ShowElement("#example", swap.Top), ), want: `hx-swap='outerHTML settle:1s show:#example:top'`, diff --git a/hx/swap/swap.go b/hx/swap/swap.go index 644950b..b13ff25 100644 --- a/hx/swap/swap.go +++ b/hx/swap/swap.go @@ -3,6 +3,7 @@ package swap import ( "fmt" + "time" "github.com/will-wow/typed-htmx-go/hx/internal/mod" ) @@ -69,17 +70,17 @@ func (s *Builder) Transition() *Builder { return s } -// SwapTiming modifies the amount of time that htmx will wait after receiving a response to swap the content. +// Swap modifies the amount of time that htmx will wait after receiving a response to swap the content. // This attribute can be used to synchronize htmx with the timing of CSS transition effects. -func (s *Builder) SwapTiming(wait string) *Builder { - s.modifiers[Swap] = wait +func (s *Builder) Swap(wait time.Duration) *Builder { + s.modifiers[Swap] = wait.String() return s } -// SettleTiming modifies the time between the swap and the settle logic. +// Settle modifies the time between the swap and the settle logic. // This attribute can be used to synchronize htmx with the timing of CSS transition effects. -func (s *Builder) SettleTiming(wait string) *Builder { - s.modifiers[Settle] = wait +func (s *Builder) Settle(wait time.Duration) *Builder { + s.modifiers[Settle] = wait.String() return s } diff --git a/hx/swap/swap_test.go b/hx/swap/swap_test.go index 7801c92..c1f4ab3 100644 --- a/hx/swap/swap_test.go +++ b/hx/swap/swap_test.go @@ -2,6 +2,7 @@ package swap_test import ( "testing" + "time" "github.com/will-wow/typed-htmx-go/hx/swap" ) @@ -11,19 +12,39 @@ func TestSwap(t *testing.T) { tests := []struct { name string - builder swap.Builder + builder *swap.Builder want string }{ { name: "Default", - builder: *swap.New(), + builder: swap.New(), want: "innerHTML", }, { name: "Strategy", - builder: *swap.New().Strategy(swap.OuterHTML), + builder: swap.New().Strategy(swap.OuterHTML), want: "outerHTML", }, + { + name: "Transition", + builder: swap.New().Transition(), + want: "innerHTML transition:true", + }, + { + name: "SwapTiming", + builder: swap.New().Swap(500 * time.Millisecond), + want: "innerHTML swap:500ms", + }, + { + name: "SettleTiming", + builder: swap.New().Settle(500 * time.Millisecond), + want: "innerHTML settle:500ms", + }, + { + name: "Clear", + builder: swap.New().Transition().Clear(swap.Transition), + want: "innerHTML", + }, } for _, tt := range tests { diff --git a/hx/trigger/event.go b/hx/trigger/event.go index 8191907..a2862ff 100644 --- a/hx/trigger/event.go +++ b/hx/trigger/event.go @@ -168,6 +168,13 @@ func (e *Event) Queue(option QueueOption) *Event { return e } +// Clear removes a modifier entirely from the builder. +// Used to undo an previously set modifier. +func (s *Event) Clear(modifier Modifier) *Event { + delete(s.modifiers, modifier) + return s +} + // coreEvent returns the event name with the filter appended, if present. func (e *Event) coreEvent() string { if e.filter == "" { diff --git a/hx/trigger/trigger_test.go b/hx/trigger/trigger_test.go index 96db87d..026eb0e 100644 --- a/hx/trigger/trigger_test.go +++ b/hx/trigger/trigger_test.go @@ -88,6 +88,11 @@ func TestNewEvent(t *testing.T) { trigger: trigger.NewEvent("click").Queue(trigger.First), want: "click queue:first", }, + { + name: "Clear", + trigger: trigger.NewEvent("click").Consume().Clear(trigger.Consume), + want: "click", + }, { name: "Ordering multiple", trigger: trigger.NewEvent("click").Filter("isActive").Queue(trigger.First).Consume().Target("#element").From("#parent > #child"),