Skip to content

Commit

Permalink
update docs for NewHX, and improve naming of relative selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
will-wow committed Mar 10, 2024
1 parent 2c347fb commit c136f13
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 106 deletions.
63 changes: 53 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Well-documented Go functions for building [HTMX](https://htmx.org) attributes.

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.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{}`.
`hx.NewTempl()` provides an `hx` struct that exposes all documented [HTMX attributes](https://htmx.org/reference/) as Go functions, and [templ.Attributes](https://templ.guide/syntax-and-usage/attributes) to be spread into a Templ element. `hx.NewGomponents()` returns an `hx` struct that exposes attributes as functions that return `g.Node` attributes instead.. You can also support other templating libraries by simply passing an `attr` function to `htmx.NewHX(attr)`.

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.

Expand All @@ -35,23 +35,46 @@ Many HTMX attributes (like `hx-swap` and `hx-trigger`) support a complex syntax

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.
`hx` strives to provide typed builders 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` 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:
For instance, many attributes (like [hx-target](https://htmx.org/attributes/hx-target/) and [hx-include](https://htmx.org/attributes/hx-include/)) support "extended selectors", which is either a standard CSS selector, or some non-standard keyword like `this` or `closest`. But different attributes support different non-standard selectors, so they have their own types; `HX.Target()` takes an `htmx.TargetSelector` that supports only `this`, `next`, or `previous`, plus optionally any of the standard relative modifiers like `closest` or `find`, while `HX.Include()` takes an `htmx.IncludeSelector` that only allows the `closest` modifier, and no non-standard selectors. Since they also take any arbitrary CSS selector, you can pass in any string, but sticking to the provided types when available makes it easier to make sure you've got valid selectors.

- `hx.Target("#element")` => `hx-target="#element'`
- `hx.TargetNonStandard(hx.TargetThis)` => `hx-target='this'`
- `hx.TargetRelative(hx.TargetSelectorNext, "#element")` => `hx-target='next #element'`
Example:

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.
```go
hx.Target("#element")
hx.Target(htmx.TargetRelative(htmx.Next, "#element"))
hx.Target(htmx.TargetNext)

hx.Include("#element")
hx.Include(IncludeThis)
hx.Include(htmx.IncludeRelative(htmx.Next, "#element"))
hx.Include(htmx.TargetNext) // Invalid: cannot use TargetNext (constant "next" of type TargetSelector) as IncludeSelector value in argument to hx.Include
```

### 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 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.
`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, and an example test.

Note: Documentation is in progress. If you see something missing, please submit a PR!

### Support many component libraries

`hx` is built to support any Go HTML templating library that can take attributes as some data type. The two supported out of the box are Templ and Gomponents, but it should be easy work with other libraries. And if you do use `typed-htmx-go` with another library, please submit a PR to add official support!

To handle the different types (Templ expects a `Templ.Attributes map[string]any`, and Gomponents wants a `g.Node` with a `Render` method), the `htmx.NewHX` constructor takes an `attr` function that takes an HTMX attribute and an `any` value, and returns some type T. That means you can construct an `hx` that returns `Templ.Attributes` from every attribute function, one that returns a `g.Node`, and another that returns whatever your library expects.

Note: This work is on going. If you see something missing, please submit a PR!
For ease of use, you should create a private `var hx` in your template packages, like so:

```go
var hx = htmx.NewTempl()

templ MyDiv() {
<button { hx.Get("/some/path")... } />
}
```

### Transferable HTMX skills

Expand Down Expand Up @@ -106,7 +129,27 @@ Form(

### Fully tested

Every attribute function 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.
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

## Install

Expand Down
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.
65 changes: 31 additions & 34 deletions htmx/htmx.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,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 {
if boost {
return hx.attr("hx-boost", "true")
}
return hx.attr("hx-boost", "false")
return hx.attr("hx-boost", 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.
Expand Down Expand Up @@ -451,8 +448,8 @@ func (hx *HX[T]) SwapOOBWithStrategy(strategy swap.Strategy) T {
// HTMX Attribute: [hx-swap-oob]
//
// [hx-swap-oob]: https://htmx.org/attributes/hx-swap-oob
func (hx *HX[T]) SwapOOBSelector(strategy swap.Strategy, extendedSelector string) T {
return hx.attr(SwapOOB, fmt.Sprintf("%s:%s", strategy, extendedSelector))
func (hx *HX[T]) SwapOOBSelector(strategy swap.Strategy, cssSelector string) T {
return hx.attr(SwapOOB, fmt.Sprintf("%s:%s", strategy, cssSelector))
}

type TargetSelector string
Expand All @@ -463,7 +460,7 @@ const (
TargetPrevious TargetSelector = "previous" // resolves to element.previousElementSibling
)

var TargetRelative = makeRelativeSelector[SelectorModifier, TargetSelector]()
var TargetRelative = makeRelativeSelector[RelativeModifier, TargetSelector]()

// Target allows you to target a different element for swapping than the one issuing the AJAX request.
//
Expand Down Expand Up @@ -672,8 +669,8 @@ var DisabledEltRelative = makeRelativeSelector[DisabledEltModifier, DisabledEltS
// HTMX Attribute: [hx-disabled-elt]
//
// [hx-disabled-elt]: https://htmx.org/attributes/hx-disabled-elt
func (hx *HX[T]) DisabledElt(selector DisabledEltSelector) T {
return hx.attr(DisabledElt, string(selector))
func (hx *HX[T]) DisabledElt(extendedSelector DisabledEltSelector) T {
return hx.attr(DisabledElt, string(extendedSelector))
}

// Disinherit allows you to disable automatic attribute inheritance for one or multiple specified attributes.
Expand Down Expand Up @@ -892,15 +889,15 @@ type IncludeSelector string

const IncludeThis IncludeSelector = "this"

var IncludeRelative = makeRelativeSelector[SelectorModifier, IncludeSelector]()
var IncludeRelative = makeRelativeSelector[RelativeModifier, IncludeSelector]()

// Include allows you to include additional element values in an AJAX request.
//
// HTMX Attribute: [hx-include]
//
// [hx-include]: https://htmx.org/attributes/hx-include/
func (hx *HX[T]) Include(selector IncludeSelector) T {
return hx.attr(Include, string(selector))
func (hx *HX[T]) Include(extendedSelector IncludeSelector) T {
return hx.attr(Include, string(extendedSelector))
}

type IndicatorModifier string
Expand Down Expand Up @@ -1127,24 +1124,11 @@ func (hx *HX[T]) RequestJS(request RequestConfigJS) T {
return hx.attr(Request, request.String())
}

type SyncStrategy string

const (
SyncDefault SyncStrategy = ""
SyncDrop SyncStrategy = "drop" // drop (ignore) this request if an existing request is in flight (the default)
SyncAbort SyncStrategy = "abort" // drop (ignore) this request if an existing request is in flight, and, if that is not the case, abort this request if another request occurs while it is still in flight
SyncReplace SyncStrategy = "replace" // abort the current request, if any, and replace it with this request
SyncQueue SyncStrategy = "queue" // place this request in the request queue associated with the given element
SyncQueueFirst SyncStrategy = "queue first" // queue the first request to show up while a request is in flight
SyncQueueLast SyncStrategy = "queue last" // queue the last request to show up while a request is in flight
SyncQueueAll SyncStrategy = "queue all" // queue all requests that show up while a request is in flight
)

type SyncSelector string

const SyncThis SyncSelector = "this"

var SyncRelative = makeRelativeSelector[SelectorModifier, SyncSelector]()
var SyncRelative = makeRelativeSelector[RelativeModifier, SyncSelector]()

// SyncStrategy allows you to synchronize AJAX requests between multiple elements.
//
Expand All @@ -1162,6 +1146,19 @@ func (hx *HX[T]) Sync(extendedSelector SyncSelector) T {
return hx.attr(Sync, string(extendedSelector))
}

type SyncStrategy string

const (
SyncDefault SyncStrategy = ""
SyncDrop SyncStrategy = "drop" // drop (ignore) this request if an existing request is in flight (the default)
SyncAbort SyncStrategy = "abort" // drop (ignore) this request if an existing request is in flight, and, if that is not the case, abort this request if another request occurs while it is still in flight
SyncReplace SyncStrategy = "replace" // abort the current request, if any, and replace it with this request
SyncQueue SyncStrategy = "queue" // place this request in the request queue associated with the given element
SyncQueueFirst SyncStrategy = "queue first" // queue the first request to show up while a request is in flight
SyncQueueLast SyncStrategy = "queue last" // queue the last request to show up while a request is in flight
SyncQueueAll SyncStrategy = "queue all" // queue all requests that show up while a request is in flight
)

// SyncStrategy allows you to synchronize AJAX requests between multiple elements.
//
// The hx-sync attribute consists of a CSS selector to indicate the element to synchronize on, followed optionally by a colon and then by an optional syncing strategy.
Expand Down Expand Up @@ -1247,19 +1244,19 @@ const (
Validate Attribute = "hx-validate"
)

// A SelectorModifier is a relative modifier to a CSS selector. This is used for "extended selectors".
// A RelativeModifier is a relative modifier to a CSS selector. This is used for "extended selectors".
// Some attributes only support a subset of these, but any Relative function that takes this type supports the full set..
type SelectorModifier string
type RelativeModifier string

const (
Closest SelectorModifier = "closest" // find the closest ancestor element or itself, that matches the given CSS selector
Find SelectorModifier = "find" // find the first child descendant element that matches the given CSS selector
Next SelectorModifier = "next" // scan the DOM forward for the first element that matches the given CSS selector. (e.g. next .error will target the closest following sibling element with error class)
Previous SelectorModifier = "previous" // scan the DOM backwards fo
Closest RelativeModifier = "closest" // find the closest ancestor element or itself, that matches the given CSS selector
Find RelativeModifier = "find" // find the first child descendant element that matches the given CSS selector
Next RelativeModifier = "next" // scan the DOM forward for the first element that matches the given CSS selector. (e.g. next .error will target the closest following sibling element with error class)
Previous RelativeModifier = "previous" // scan the DOM backwards fo
)

func boolToString(hx bool) string {
if hx {
func boolToString(b bool) string {
if b {
return "true"
}
return "false"
Expand Down
23 changes: 13 additions & 10 deletions htmx/swap/swap.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ type Builder struct {
modifiers map[Modifier]string
}

// New starts a builder chain for creating a new hx-swap attribute.
// New starts a builder chain for creating a new [hx-swap] attribute.
// It contains methods to add and remove modifiers from the swap.
// Subsequent calls can override previous modifiers of the same type - for instance, .Scroll(Top).Scroll(Bottom) will result in `hx-swap="scroll:bottom"`.
// Call .End() to get the final hx-swap string.
// Pass to [htmx.HX.SwapExtended] to set the swap attribute on an element.
//
// [hx-swap]: https://htmx.org/attributes/hx-swap/
func New() *Builder {
return &Builder{
strategy: InnerHTML,
Expand Down Expand Up @@ -118,16 +120,17 @@ func (s *Builder) Show(scrollDirection ScrollDirection) *Builder {
return s
}

// A ShowSelector is a CSS selector that identifies the element to show. Includes the non-standard "window" value to scroll the viewport to the top/bottom after a swap.
type ShowSelector string

const (
ShowWindow ShowSelector = "window"
)

// ShowElement will scroll the viewport to show the selected element after the swap.
// The selector is a CSS selector that identifies the element to show.
func (s *Builder) ShowElement(selector string, scrollDirection ScrollDirection) *Builder {
s.modifiers[Show] = fmt.Sprintf("%s:%s", selector, scrollDirection)
return s
}

// ShowWindow will scroll the viewport to the top/bottom after the swap.
func (s *Builder) ShowWindow(scrollDirection ScrollDirection) *Builder {
s.modifiers[Show] = fmt.Sprintf("window:%s", scrollDirection)
func (s *Builder) ShowElement(extendedSelector ShowSelector, scrollDirection ScrollDirection) *Builder {
s.modifiers[Show] = fmt.Sprintf("%s:%s", extendedSelector, scrollDirection)
return s
}

Expand Down
Loading

0 comments on commit c136f13

Please sign in to comment.