Skip to content

Commit

Permalink
update readme and tweak a couple function names to be closer to htmx
Browse files Browse the repository at this point in the history
  • Loading branch information
will-wow committed Feb 22, 2024
1 parent 8f5f02d commit e532bb0
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 14 deletions.
104 changes: 101 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed-htmx-go
# typed-htmx-go/hx

Well-documented Go functions for building [HTMX](https://htmx.org) attributes.

Expand All @@ -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
<form
method="GET"
action="/page"
hx-get="/page"
hx-target="body"
hx-replace-url="true"
hx-swap="scroll:#search-results:top swap:1s"
></form>
```

The `hx` equivalent should take the same names and values in the same order:

```go
<form
method="GET"
action="/page"
{ hx.New().
Get("/page").
Target("body").
ReplaceURL(true).
Swap(
swap.New().
ScrollElement("#search-results", swap.Top).
Swap(time.Second),
).
Build()... }
>
```

### 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
Expand All @@ -30,7 +110,7 @@ import (
templ SearchInput(search string) {
<form
method="GET"
action={ templ.URL(currentPage) }
action="/page"
class="relative mb-2"
{ hx.New().
Get(currentPage).
Expand Down Expand Up @@ -66,3 +146,21 @@ templ SearchInput(search string) {
```bash
go install github.com/go-task/task/v3/cmd/task@latest
```

### Install tools

```bash
task tools
```

### Check that everything is ready to commit, and update the coverage badge

```bash
task ready
```

### Publish

```bash
VERSION="0.0.0" task publish
```
4 changes: 4 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,9 @@ tasks:
tools:
desc: Install tools
cmds:
# Install goimports for formatting
- go install golang.org/x/tools/cmd/goimports@latest
# Install golangci for linting
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
# Install prettier for formatting non-go files
- npm install
2 changes: 1 addition & 1 deletion assets/badge.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions hx/hx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//
// <button {hx.New().Delete("/account").Confirm("Are you sure you wish to delete you account?").Build()...}>
// Delete My Account
// </button>
//
// # 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
Expand Down
2 changes: 1 addition & 1 deletion hx/hx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'`,
Expand Down
13 changes: 7 additions & 6 deletions hx/swap/swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package swap

import (
"fmt"
"time"

"github.com/will-wow/typed-htmx-go/hx/internal/mod"
)
Expand Down Expand Up @@ -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
}

Expand Down
27 changes: 24 additions & 3 deletions hx/swap/swap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package swap_test

import (
"testing"
"time"

"github.com/will-wow/typed-htmx-go/hx/swap"
)
Expand All @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions hx/trigger/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down
5 changes: 5 additions & 0 deletions hx/trigger/trigger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down

0 comments on commit e532bb0

Please sign in to comment.