cleaning up the api a bit

This commit is contained in:
maddalax 2024-09-20 22:59:07 -05:00
parent e34a4fe269
commit c555da4dc9
22 changed files with 641 additions and 218 deletions

View file

@ -22,6 +22,8 @@ func Build() {
// }, // },
//) //)
process.RunOrExit("env GOOS=linux GOARCH=amd64 go build -o ./dist .") process.RunOrExit("env GOOS=linux GOARCH=amd64 go build -o ./dist/app-linux-amd64 .")
process.RunOrExit("go build -o ./dist/app .")
process.RunOrExit("echo \"Build successful\"") process.RunOrExit("echo \"Build successful\"")
} }

View file

@ -2,6 +2,7 @@ package ui
import ( import (
"github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/hx"
) )
type InputProps struct { type InputProps struct {
@ -16,8 +17,7 @@ type InputProps struct {
func Input(props InputProps) h.Ren { func Input(props InputProps) h.Ren {
validation := h.If(props.ValidationPath != "", h.Children( validation := h.If(props.ValidationPath != "", h.Children(
h.Post(props.ValidationPath), h.Post(props.ValidationPath, hx.ChangeEvent),
h.Trigger("change"),
h.Attribute("hx-swap", "innerHTML transition:true"), h.Attribute("hx-swap", "innerHTML transition:true"),
h.Attribute("hx-target", "next div"), h.Attribute("hx-target", "next div"),
)) ))

File diff suppressed because one or more lines are too long

View file

@ -6,9 +6,24 @@ let lastVersion = "";
htmx.defineExtension("livereload", { htmx.defineExtension("livereload", {
init: function () { init: function () {
const host = window.location.host; const host = window.location.host;
let enabled = false
for (const element of Array.from(htmx.findAll("[hx-ext]"))) {
const value = element.getAttribute("hx-ext");
if(value?.split(" ").includes("livereload")) {
enabled = true
break;
}
}
if(!enabled) {
return
}
console.log('livereload extension initialized.'); console.log('livereload extension initialized.');
createWebSocketClient({ createWebSocketClient({
url: `ws://${host}/dev/livereload`, url: `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${host}/dev/livereload`,
onOpen: () => { onOpen: () => {
}, },
onMessage: (message) => { onMessage: (message) => {

View file

@ -55,6 +55,6 @@ func GetPartialPath(partial func(ctx *RequestContext) *Partial) string {
return runtime.FuncForPC(reflect.ValueOf(partial).Pointer()).Name() return runtime.FuncForPC(reflect.ValueOf(partial).Pointer()).Name()
} }
func GetPartialPathWithQs(partial func(ctx *RequestContext) *Partial, qs string) string { func GetPartialPathWithQs(partial func(ctx *RequestContext) *Partial, qs *Qs) string {
return html.EscapeString(GetPartialPath(partial) + "?" + qs) return html.EscapeString(GetPartialPath(partial) + "?" + qs.ToString())
} }

View file

@ -0,0 +1,32 @@
package h
func If(condition bool, node Ren) Ren {
if condition {
return node
} else {
return Empty()
}
}
func IfElse(condition bool, node Ren, node2 Ren) Ren {
if condition {
return node
} else {
return node2
}
}
func IfElseLazy(condition bool, cb1 func() Ren, cb2 func() Ren) Ren {
if condition {
return cb1()
} else {
return cb2()
}
}
func IfHtmxRequest(ctx *RequestContext, node Ren) Ren {
if ctx.Get("HX-Request") != "" {
return node
}
return Empty()
}

View file

@ -1,24 +0,0 @@
package h
type HxEvent = string
type HxTriggerName = string
var (
HxBeforeRequest HxEvent = "hx-on::before-request"
HxAfterRequest HxEvent = "hx-on::after-request"
HxOnMutationError HxEvent = "hx-on::mutation-error"
HxOnLoad HxEvent = "hx-on::load"
HxOnLoadError HxEvent = "hx-on::load-error"
HxRequestTimeout HxEvent = "hx-on::request-timeout"
HxTrigger HxEvent = "hx-on::trigger"
HxRequestStart HxEvent = "hx-on::xhr:loadstart"
HxRequestProgress HxEvent = "hx-on::xhr:progress"
)
const (
TriggerLoad HxTriggerName = "load"
TriggerClick HxTriggerName = "click"
TriggerDblClick HxTriggerName = "dblclick"
TriggerKeyUpEnter HxTriggerName = "keyup[keyCode==13]"
TriggerBlur HxTriggerName = "blur"
)

View file

@ -2,61 +2,104 @@ package h
import ( import (
"fmt" "fmt"
"github.com/maddalax/htmgo/framework/hx"
"strings"
) )
type LifeCycle struct { type LifeCycle struct {
handlers map[HxEvent][]JsCommand handlers map[hx.Event][]Command
} }
func NewLifeCycle() *LifeCycle { func NewLifeCycle() *LifeCycle {
return &LifeCycle{ return &LifeCycle{
handlers: make(map[HxEvent][]JsCommand), handlers: make(map[hx.Event][]Command),
} }
} }
func (l *LifeCycle) OnEvent(event HxEvent, cmd ...JsCommand) *LifeCycle { func validateCommands(cmds []Command) {
if l.handlers[event] == nil { for _, cmd := range cmds {
l.handlers[event] = []JsCommand{} switch t := cmd.(type) {
case JsCommand:
break
case *AttributeMap:
break
case *Element:
panic(fmt.Sprintf("element is not allowed in lifecycle events. Got: %v", t))
default:
panic(fmt.Sprintf("type is not allowed in lifecycle events. Got: %v", t))
}
} }
}
func (l *LifeCycle) OnEvent(event hx.Event, cmd ...Command) *LifeCycle {
validateCommands(cmd)
if l.handlers[event] == nil {
l.handlers[event] = []Command{}
}
l.handlers[event] = append(l.handlers[event], cmd...) l.handlers[event] = append(l.handlers[event], cmd...)
return l return l
} }
func (l *LifeCycle) BeforeRequest(cmd ...JsCommand) *LifeCycle { func (l *LifeCycle) BeforeRequest(cmd ...Command) *LifeCycle {
l.OnEvent(HxBeforeRequest, cmd...) l.OnEvent(hx.BeforeRequestEvent, cmd...)
return l return l
} }
func OnEvent(event HxEvent, cmd ...JsCommand) *LifeCycle { func OnLoad(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.LoadEvent, cmd...)
}
func OnAfterSwap(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.AfterSwapEvent, cmd...)
}
func OnTrigger(trigger string, cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.NewStringTrigger(trigger).ToString(), cmd...)
}
func OnClick(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.ClickEvent, cmd...)
}
func OnEvent(event hx.Event, cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(event, cmd...) return NewLifeCycle().OnEvent(event, cmd...)
} }
func BeforeRequest(cmd ...JsCommand) *LifeCycle { func BeforeRequest(cmd ...Command) *LifeCycle {
return NewLifeCycle().BeforeRequest(cmd...) return NewLifeCycle().BeforeRequest(cmd...)
} }
func AfterRequest(cmd ...JsCommand) *LifeCycle { func AfterRequest(cmd ...Command) *LifeCycle {
return NewLifeCycle().AfterRequest(cmd...) return NewLifeCycle().AfterRequest(cmd...)
} }
func OnMutationError(cmd ...JsCommand) *LifeCycle { func OnMutationError(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnMutationError(cmd...) return NewLifeCycle().OnMutationError(cmd...)
} }
func (l *LifeCycle) AfterRequest(cmd ...JsCommand) *LifeCycle { func (l *LifeCycle) AfterRequest(cmd ...Command) *LifeCycle {
l.OnEvent(HxAfterRequest, cmd...) l.OnEvent(hx.AfterRequestEvent, cmd...)
return l return l
} }
func (l *LifeCycle) OnMutationError(cmd ...JsCommand) *LifeCycle { func (l *LifeCycle) OnMutationError(cmd ...Command) *LifeCycle {
l.OnEvent(HxOnMutationError, cmd...) l.OnEvent(hx.OnMutationErrorEvent, cmd...)
return l return l
} }
type Command = Ren
type JsCommand struct { type JsCommand struct {
Command string Command string
} }
func (j JsCommand) Render(builder *strings.Builder) {
builder.WriteString(j.Command)
}
func SetText(text string) JsCommand { func SetText(text string) JsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.innerText = '%s'", text)} return JsCommand{Command: fmt.Sprintf("this.innerText = '%s'", text)}

View file

@ -2,6 +2,7 @@ package h
import ( import (
"fmt" "fmt"
"github.com/maddalax/htmgo/framework/hx"
"strings" "strings"
) )
@ -89,20 +90,61 @@ func (m *AttributeMap) Render(builder *strings.Builder) {
} }
} }
func toHtmxTriggerName(event string) string {
if strings.HasPrefix(event, "htmx:") {
return event[5:]
}
if strings.HasPrefix(event, "on") {
return event[2:]
}
return event
}
func formatEventName(event string, isDomEvent bool) string {
raw := toHtmxTriggerName(event)
if isDomEvent {
return "on" + raw
}
return event
}
func (l *LifeCycle) fromAttributeMap(event string, key string, value string, builder *strings.Builder) {
if key == hx.GetAttr || key == hx.PatchAttr || key == hx.PostAttr {
TriggerString(toHtmxTriggerName(event)).Render(builder)
}
Attribute(key, value).Render(builder)
}
func (l *LifeCycle) Render(builder *strings.Builder) { func (l *LifeCycle) Render(builder *strings.Builder) {
m := make(map[string]string) m := make(map[string]string)
for event, commands := range l.handlers { for event, commands := range l.handlers {
m[event] = "" m[event] = ""
for _, command := range commands { for _, command := range commands {
m[event] += fmt.Sprintf("%s;", command.Command) switch c := command.(type) {
case JsCommand:
eventName := formatEventName(event, true)
m[eventName] += fmt.Sprintf("%s;", c.Command)
case *AttributeMap:
for k, v := range c.ToMap() {
l.fromAttributeMap(event, k, v, builder)
}
}
} }
} }
children := make([]Ren, 0) children := make([]Ren, 0)
for event, js := range m { for event, value := range m {
children = append(children, Attribute(event, js)) if value != "" {
children = append(children, Attribute(event, value))
}
}
if len(children) == 0 {
return
} }
Children(children...).Render(builder) Children(children...).Render(builder)

View file

@ -3,11 +3,51 @@ package h
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html" "github.com/maddalax/htmgo/framework/hx"
"net/url" "net/url"
"strings" "strings"
) )
type Qs struct {
m map[string]string
}
func NewQs(pairs ...string) *Qs {
q := &Qs{
m: make(map[string]string),
}
if len(pairs)%2 != 0 {
return q
}
for i := 0; i < len(pairs); i++ {
q.m[pairs[i]] = pairs[i+1]
i++
}
return q
}
func (q *Qs) Add(key string, value string) *Qs {
q.m[key] = value
return q
}
func (q *Qs) ToString() string {
builder := strings.Builder{}
index := 0
for k, v := range q.m {
builder.WriteString(k)
builder.WriteString("=")
builder.WriteString(v)
if index < len(q.m)-1 {
builder.WriteString("&")
}
index++
}
return builder.String()
}
type PartialFunc = func(ctx *RequestContext) *Partial
type Element struct { type Element struct {
tag string tag string
attributes map[string]string attributes map[string]string
@ -76,12 +116,24 @@ func Attributes(attrs *AttributeMap) *AttributeMap {
return attrs return attrs
} }
func AttributePairs(pairs ...string) *AttributeMap {
if len(pairs)%2 != 0 {
return &AttributeMap{}
}
m := make(AttributeMap)
for i := 0; i < len(pairs); i++ {
m[pairs[i]] = pairs[i+1]
i++
}
return &m
}
func Checked() Ren { func Checked() Ren {
return Attribute("checked", "true") return Attribute("checked", "true")
} }
func Boost() Ren { func Boost() Ren {
return Attribute("hx-boost", "true") return Attribute(hx.BoostAttr, "true")
} }
func Attribute(key string, value string) *AttributeMap { func Attribute(key string, value string) *AttributeMap {
@ -93,90 +145,24 @@ func TriggerChildren() Ren {
} }
func HxExtension(value string) Ren { func HxExtension(value string) Ren {
return Attribute("hx-ext", value) return Attribute(hx.ExtAttr, value)
} }
func Disabled() Ren { func Disabled() Ren {
return Attribute("disabled", "") return Attribute("disabled", "")
} }
func Get(path string) Ren { func TriggerString(triggers ...string) *AttributeMap {
return Attribute("hx-get", path) trigger := hx.NewStringTrigger(strings.Join(triggers, ", "))
return Attribute(hx.TriggerAttr, trigger.ToString())
} }
func GetPartial(partial func(ctx *RequestContext) *Partial) Ren { func Trigger(opts ...hx.TriggerEvent) *AttributeMap {
return Get(GetPartialPath(partial)) return Attribute(hx.TriggerAttr, hx.NewTrigger(opts...).ToString())
} }
func GetPartialWithQs(partial func(ctx *RequestContext) *Partial, qs string) Ren { func TriggerClick(opts ...hx.Modifier) *AttributeMap {
return Get(GetPartialPathWithQs(partial, qs)) return Trigger(hx.OnClick(opts...))
}
func CreateTriggers(triggers ...string) []string {
return triggers
}
type ReloadParams struct {
Triggers []string
Target string
Children Ren
}
func ViewOnLoad(partial func(ctx *RequestContext) *Partial) Ren {
return View(partial, ReloadParams{
Triggers: CreateTriggers("load"),
})
}
func View(partial func(ctx *RequestContext) *Partial, params ReloadParams) Ren {
return Div(Attributes(&AttributeMap{
"hx-get": GetPartialPath(partial),
"hx-trigger": strings.Join(params.Triggers, ", "),
"hx-target": params.Target,
}), params.Children)
}
func PartialWithTriggers(partial func(ctx *RequestContext) *Partial, triggers ...string) Ren {
return Div(Attributes(&AttributeMap{
"hx-get": GetPartialPath(partial),
"hx-trigger": strings.Join(triggers, ", "),
}))
}
func GetWithQs(path string, qs map[string]string) Ren {
return Get(SetQueryParams(path, qs))
}
func PostPartialOnTrigger(partial func(ctx *RequestContext) *Partial, triggers ...string) Ren {
return PostOnTrigger(GetPartialPath(partial), strings.Join(triggers, ", "))
}
func PostPartialWithQsOnTrigger(partial func(ctx *RequestContext) *Partial, qs string, trigger string) Ren {
return PostOnTrigger(GetPartialPathWithQs(partial, qs), trigger)
}
func Post(url string) Ren {
return Attribute("hx-post", url)
}
func PostOnTrigger(url string, trigger string) Ren {
return AttributeList(Attribute("hx-post", url), Trigger(trigger))
}
func PostOnClick(url string) Ren {
return PostOnTrigger(url, "click")
}
func PostPartialOnClick(partial func(ctx *RequestContext) *Partial) Ren {
return PostOnClick(GetPartialPath(partial))
}
func PostPartialOnClickQs(partial func(ctx *RequestContext) *Partial, qs string) Ren {
return PostOnClick(GetPartialPathWithQs(partial, qs))
}
func Trigger(trigger string) *AttributeMap {
return Attribute("hx-trigger", trigger)
} }
func TextF(format string, args ...interface{}) Ren { func TextF(format string, args ...interface{}) Ren {
@ -192,7 +178,7 @@ func Pf(format string, args ...interface{}) Ren {
} }
func Target(target string) Ren { func Target(target string) Ren {
return Attribute("hx-target", target) return Attribute(hx.TargetAttr, target)
} }
func Name(name string) Ren { func Name(name string) Ren {
@ -200,7 +186,7 @@ func Name(name string) Ren {
} }
func Confirm(message string) Ren { func Confirm(message string) Ren {
return Attribute("hx-confirm", message) return Attribute(hx.ConfirmAttr, message)
} }
func Href(path string) Ren { func Href(path string) Ren {
@ -216,7 +202,7 @@ func Placeholder(placeholder string) Ren {
} }
func OutOfBandSwap(selector string) Ren { func OutOfBandSwap(selector string) Ren {
return Attribute("hx-swap-oob", return Attribute(hx.SwapOobAttr,
Ternary(selector == "", "true", selector)) Ternary(selector == "", "true", selector))
} }
@ -307,7 +293,7 @@ func Article(children ...Ren) *Element {
} }
func ReplaceUrlHeader(url string) *Headers { func ReplaceUrlHeader(url string) *Headers {
return NewHeaders("HX-Replace-Url", url) return NewHeaders(hx.ReplaceUrlHeader, url)
} }
func CombineHeaders(headers ...*Headers) *Headers { func CombineHeaders(headers ...*Headers) *Headers {
@ -321,7 +307,7 @@ func CombineHeaders(headers ...*Headers) *Headers {
} }
func CurrentPath(ctx *RequestContext) string { func CurrentPath(ctx *RequestContext) string {
current := ctx.Request().Header.Get("Hx-Current-Url") current := ctx.Request().Header.Get(hx.CurrentUrlHeader)
parsed, err := url.Parse(current) parsed, err := url.Parse(current)
if err != nil { if err != nil {
return "" return ""
@ -330,12 +316,12 @@ func CurrentPath(ctx *RequestContext) string {
} }
func PushQsHeader(ctx *RequestContext, key string, value string) *Headers { func PushQsHeader(ctx *RequestContext, key string, value string) *Headers {
current := ctx.Request().Header.Get("Hx-Current-Url") current := ctx.Request().Header.Get(hx.CurrentUrlHeader)
parsed, err := url.Parse(current) parsed, err := url.Parse(current)
if err != nil { if err != nil {
return NewHeaders() return NewHeaders()
} }
return NewHeaders("HX-Replace-Url", SetQueryParams(parsed.Path, map[string]string{ return NewHeaders(hx.ReplaceUrlHeader, SetQueryParams(parsed.Path, map[string]string{
key: value, key: value,
})) }))
} }
@ -402,8 +388,8 @@ func Button(children ...Ren) *Element {
return Tag("button", children...) return Tag("button", children...)
} }
func Indicator(tag string) Ren { func Indicator(tag string) *AttributeMap {
return Attribute("hx-indicator", tag) return Attribute(hx.IndicatorAttr, tag)
} }
func P(children ...Ren) *Element { func P(children ...Ren) *Element {
@ -460,31 +446,6 @@ func Empty() *Element {
} }
} }
func BeforeRequestSetHtml(children ...Ren) Ren {
serialized := Render(Fragment(children...))
return Attribute("hx-on::before-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
}
func BeforeRequestSetAttribute(key string, value string) Ren {
return Attribute("hx-on::before-request", `this.setAttribute('`+key+`', '`+value+`')`)
}
func OnMutationErrorSetText(text string) Ren {
return Attribute("hx-on::mutation-error", `this.innerText = '`+text+`'`)
}
func BeforeRequestSetText(text string) Ren {
return Attribute("hx-on::before-request", `this.innerText = '`+text+`'`)
}
func AfterRequestSetText(text string) Ren {
return Attribute("hx-on::after-request", `this.innerText = '`+text+`'`)
}
func AfterRequestRemoveAttribute(key string, value string) Ren {
return Attribute("hx-on::after-request", `this.removeAttribute('`+key+`')`)
}
func IfQueryParam(key string, node *Element) Ren { func IfQueryParam(key string, node *Element) Ren {
return Fragment(Attribute("hx-if-qp:"+key, "true"), node) return Fragment(Attribute("hx-if-qp:"+key, "true"), node)
} }
@ -493,11 +454,6 @@ func Hidden() Ren {
return Attribute("style", "display:none") return Attribute("style", "display:none")
} }
func AfterRequestSetHtml(children ...Ren) Ren {
serialized := Render(Fragment(children...))
return Attribute("hx-on::after-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
}
func Children(children ...Ren) *ChildList { func Children(children ...Ren) *ChildList {
return NewChildList(children...) return NewChildList(children...)
} }
@ -506,41 +462,10 @@ func Label(text string) *Element {
return Tag("label", Text(text)) return Tag("label", Text(text))
} }
func If(condition bool, node Ren) Ren {
if condition {
return node
} else {
return Empty()
}
}
func IfElse(condition bool, node Ren, node2 Ren) Ren {
if condition {
return node
} else {
return node2
}
}
func IfElseLazy(condition bool, cb1 func() Ren, cb2 func() Ren) Ren {
if condition {
return cb1()
} else {
return cb2()
}
}
func GetTriggerName(ctx *RequestContext) string { func GetTriggerName(ctx *RequestContext) string {
return ctx.Request().Header.Get("HX-Trigger-Name") return ctx.Request().Header.Get("HX-Trigger-Name")
} }
func IfHtmxRequest(ctx *RequestContext, node Ren) Ren {
if ctx.Get("HX-Request") != "" {
return node
}
return Empty()
}
type SwapArg struct { type SwapArg struct {
Selector string Selector string
Content *Element Content *Element
@ -575,20 +500,3 @@ func SwapMany(ctx *RequestContext, args ...SwapArg) Ren {
return arg.Content return arg.Content
})...) })...)
} }
type OnRequestSwapArgs struct {
Target string
Get string
Default *Element
BeforeRequest *Element
AfterRequest *Element
}
func OnRequestSwap(args OnRequestSwapArgs) Ren {
return Div(args.Default,
BeforeRequestSetHtml(args.BeforeRequest),
AfterRequestSetHtml(args.AfterRequest),
Get(args.Get),
Target(args.Target),
)
}

43
framework/h/xhr.go Normal file
View file

@ -0,0 +1,43 @@
package h
import "github.com/maddalax/htmgo/framework/hx"
func Get(path string, trigger ...string) *AttributeMap {
return AttributeList(Attribute(hx.GetAttr, path), TriggerString(trigger...))
}
func GetPartial(partial PartialFunc, trigger ...string) *AttributeMap {
return Get(GetPartialPath(partial), trigger...)
}
func GetPartialWithQs(partial PartialFunc, qs *Qs, trigger string) *AttributeMap {
return Get(GetPartialPathWithQs(partial, qs), trigger)
}
func GetWithQs(path string, qs map[string]string, trigger string) *AttributeMap {
return Get(SetQueryParams(path, qs), trigger)
}
func PostPartial(partial PartialFunc, triggers ...string) *AttributeMap {
return Post(GetPartialPath(partial), triggers...)
}
func PostPartialWithQs(partial PartialFunc, qs *Qs, trigger ...string) *AttributeMap {
return Post(GetPartialPathWithQs(partial, qs), trigger...)
}
func Post(url string, trigger ...string) *AttributeMap {
return AttributeList(Attribute(hx.PostAttr, url), TriggerString(trigger...))
}
func PostOnClick(url string) *AttributeMap {
return Post(url, hx.ClickEvent)
}
func PostPartialOnClick(partial PartialFunc) *AttributeMap {
return PostOnClick(GetPartialPath(partial))
}
func PostPartialOnClickQs(partial PartialFunc, qs *Qs) *AttributeMap {
return PostOnClick(GetPartialPathWithQs(partial, qs))
}

26
framework/hx/event.go Normal file
View file

@ -0,0 +1,26 @@
package hx
import "fmt"
func OnEvent(event Event, modifiers ...Modifier) TriggerEvent {
return TriggerEvent{
event: event,
modifiers: modifiers,
}
}
func OnClick(modifiers ...Modifier) TriggerEvent {
return OnEvent(ClickEvent, modifiers...)
}
func OnLoad(modifiers ...Modifier) TriggerEvent {
return OnEvent(LoadEvent, modifiers...)
}
func OnChange(modifiers ...Modifier) TriggerEvent {
return OnEvent(ChangeEvent, modifiers...)
}
func OnPoll(durationSeconds int) TriggerEvent {
return OnEvent(PollingEvent, StringModifier(fmt.Sprintf("%ds", durationSeconds)))
}

138
framework/hx/htmx.go Normal file
View file

@ -0,0 +1,138 @@
package hx
type Attribute = string
type Header = string
type Event = string
// https://htmx.org/reference/#events
const (
GetAttr Attribute = "hx-get"
PostAttr Attribute = "hx-post"
PushUrlAttr Attribute = "hx-push-url"
SelectAttr Attribute = "hx-select"
SelectOobAttr Attribute = "hx-select-oob"
SwapAttr Attribute = "hx-swap"
SwapOobAttr Attribute = "hx-swap-oob"
TargetAttr Attribute = "hx-target"
TriggerAttr Attribute = "hx-trigger"
ValsAttr Attribute = "hx-vals"
BoostAttr Attribute = "hx-boost"
ConfirmAttr Attribute = "hx-confirm"
DeleteAttr Attribute = "hx-delete"
DisableAttr Attribute = "hx-disable"
DisabledEltAttr Attribute = "hx-disabled-elt"
DisinheritAttr Attribute = "hx-disinherit"
EncodingAttr Attribute = "hx-encoding"
ExtAttr Attribute = "hx-ext"
HeadersAttr Attribute = "hx-headers"
HistoryAttr Attribute = "hx-history"
HistoryEltAttr Attribute = "hx-history-elt"
IncludeAttr Attribute = "hx-include"
IndicatorAttr Attribute = "hx-indicator"
InheritAttr Attribute = "hx-inherit"
ParamsAttr Attribute = "hx-params"
PatchAttr Attribute = "hx-patch"
PreserveAttr Attribute = "hx-preserve"
PromptAttr Attribute = "hx-prompt"
PutAttr Attribute = "hx-put"
ReplaceUrlAttr Attribute = "hx-replace-url"
RequestAttr Attribute = "hx-request"
SyncAttr Attribute = "hx-sync"
ValidateAttr Attribute = "hx-validate"
)
const (
LocationHeader Header = "HX-Location"
PushUrlHeader Header = "HX-Push-Url"
RedirectHeader Header = "HX-Redirect"
RefreshHeader Header = "HX-Refresh"
ReplaceUrlHeader Header = "HX-Replace-Url"
CurrentUrlHeader Header = "HX-Current-Url"
ReswapHeader Header = "HX-Reswap"
RetargetHeader Header = "HX-Retarget"
ReselectHeader Header = "HX-Reselect"
TriggerHeader Header = "HX-Trigger"
TriggerAfterSettleHeader Header = "HX-Trigger-After-Settle"
TriggerAfterSwapHeader Header = "HX-Trigger-After-Swap"
)
const (
// AbortEvent Htmx Events
AbortEvent Event = "htmx:abort"
AfterOnLoadEvent Event = "htmx:afterOnLoad"
AfterProcessNodeEvent Event = "htmx:afterProcessNode"
AfterRequestEvent Event = "htmx:afterRequest"
OnMutationErrorEvent Event = "htmx:onMutationError"
AfterSettleEvent Event = "htmx:afterSettle"
AfterSwapEvent Event = "htmx:afterSwap"
BeforeCleanupElementEvent Event = "htmx:beforeCleanupElement"
BeforeOnLoadEvent Event = "htmx:beforeOnLoad"
BeforeProcessNodeEvent Event = "htmx:beforeProcessNode"
BeforeRequestEvent Event = "htmx:beforeRequest"
BeforeSwapEvent Event = "htmx:beforeSwap"
BeforeSendEvent Event = "htmx:beforeSend"
ConfigRequestEvent Event = "htmx:configRequest"
ConfirmEvent Event = "htmx:confirm"
HistoryCacheErrorEvent Event = "htmx:historyCacheError"
HistoryCacheMissEvent Event = "htmx:historyCacheMiss"
HistoryCacheMissErrorEvent Event = "htmx:historyCacheMissError"
HistoryCacheMissLoadEvent Event = "htmx:historyCacheMissLoad"
HistoryRestoreEvent Event = "htmx:historyRestore"
BeforeHistorySaveEvent Event = "htmx:beforeHistorySave"
LoadEvent Event = "htmx:load"
NoSSESourceErrorEvent Event = "htmx:noSSESourceError"
OnLoadErrorEvent Event = "htmx:onLoadError"
OobAfterSwapEvent Event = "htmx:oobAfterSwap"
OobBeforeSwapEvent Event = "htmx:oobBeforeSwap"
OobErrorNoTargetEvent Event = "htmx:oobErrorNoTarget"
PromptEvent Event = "htmx:prompt"
PushedIntoHistoryEvent Event = "htmx:pushedIntoHistory"
ResponseErrorEvent Event = "htmx:responseError"
SendErrorEvent Event = "htmx:sendError"
SSEErrorEvent Event = "htmx:sseError"
SSEOpenEvent Event = "htmx:sseOpen"
SwapErrorEvent Event = "htmx:swapError"
TargetErrorEvent Event = "htmx:targetError"
TimeoutEvent Event = "htmx:timeout"
ValidationValidateEvent Event = "htmx:validation:validate"
ValidationFailedEvent Event = "htmx:validation:failed"
ValidationHaltedEvent Event = "htmx:validation:halted"
XhrAbortEvent Event = "htmx:xhr:abort"
XhrLoadEndEvent Event = "htmx:xhr:loadend"
XhrLoadStartEvent Event = "htmx:xhr:loadstart"
XhrProgressEvent Event = "htmx:xhr:progress"
// RevealedEvent Misc Events
RevealedEvent Event = "revealed"
InstersectEvent Event = "intersect"
PollingEvent Event = "every"
// ClickEvent Dom Events
ClickEvent Event = "onclick"
ChangeEvent Event = "onchange"
InputEvent Event = "oninput"
FocusEvent Event = "onfocus"
BlurEvent Event = "onblur"
KeyDownEvent Event = "onkeydown"
KeyUpEvent Event = "onkeyup"
KeyPressEvent Event = "onkeypress"
SubmitEvent Event = "onsubmit"
LoadDomEvent Event = "onload"
UnloadEvent Event = "onunload"
ResizeEvent Event = "onresize"
ScrollEvent Event = "onscroll"
DblClickEvent Event = "ondblclick"
MouseOverEvent Event = "onmouseover"
MouseOutEvent Event = "onmouseout"
MouseMoveEvent Event = "onmousemove"
MouseDownEvent Event = "onmousedown"
MouseUpEvent Event = "onmouseup"
ContextMenuEvent Event = "oncontextmenu"
DragStartEvent Event = "ondragstart"
DragEvent Event = "ondrag"
DragEnterEvent Event = "ondragenter"
DragLeaveEvent Event = "ondragleave"
DragOverEvent Event = "ondragover"
DropEvent Event = "ondrop"
DragEndEvent Event = "ondragend"
)

19
framework/hx/htmx_test.go Normal file
View file

@ -0,0 +1,19 @@
package hx
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewStringTrigger(t *testing.T) {
trigger := "click once, htmx:click throttle:5, load delay:10"
tgr := NewStringTrigger(trigger)
assert.Equal(t, len(tgr.events), 3)
assert.Equal(t, tgr.events[0].event, "click")
assert.Equal(t, tgr.events[0].modifiers[0].Modifier(), "once")
assert.Equal(t, tgr.events[1].event, "click")
assert.Equal(t, tgr.events[1].modifiers[0].Modifier(), "throttle:5")
assert.Equal(t, tgr.events[2].event, "load")
assert.Equal(t, tgr.events[2].modifiers[0].Modifier(), "delay:10")
assert.Equal(t, "click once, click throttle:5, load delay:10", tgr.ToString())
}

49
framework/hx/modifiers.go Normal file
View file

@ -0,0 +1,49 @@
package hx
import "fmt"
type Modifier interface {
Modifier() string
}
type RawModifier struct {
modifier string
}
func StringModifier(modifier string) RawModifier {
return RawModifier{modifier}
}
func (r RawModifier) Modifier() string {
return r.modifier
}
type OnceModifier struct{}
func (o OnceModifier) Modifier() string {
return "once"
}
type ThrottleModifier struct {
durationSeconds int
}
func (t ThrottleModifier) Modifier() string {
return fmt.Sprintf("throttle:%ds", t.durationSeconds)
}
func Throttle(durationSeconds int) ThrottleModifier {
return ThrottleModifier{durationSeconds}
}
type DelayModifier struct {
durationSeconds int
}
func (t DelayModifier) Modifier() string {
return fmt.Sprintf("delay:%ds", t.durationSeconds)
}
func Delay(durationSeconds int) DelayModifier {
return DelayModifier{durationSeconds}
}

80
framework/hx/trigger.go Normal file
View file

@ -0,0 +1,80 @@
package hx
import (
"strings"
)
type Trigger struct {
events []TriggerEvent
}
type TriggerEvent struct {
event Event
modifiers []Modifier
}
func NewTrigger(opts ...TriggerEvent) *Trigger {
t := Trigger{
events: make([]TriggerEvent, 0),
}
if len(opts) > 0 {
t.events = opts
}
return &t
}
func NewStringTrigger(trigger string) Trigger {
t := Trigger{
events: make([]TriggerEvent, 0),
}
split := strings.Split(trigger, ", ")
for _, s := range split {
parts := strings.Split(s, " ")
event := parts[0]
if strings.HasPrefix(event, "htmx:") {
event = event[5:]
}
modifiers := make([]Modifier, 0)
if len(parts) > 1 {
for _, m := range parts[1:] {
modifiers = append(modifiers, RawModifier{modifier: m})
}
}
t.events = append(t.events, TriggerEvent{
event: event,
modifiers: modifiers,
})
}
return t
}
func (t Trigger) AddEvent(event TriggerEvent) Trigger {
t.events = append(t.events, event)
return t
}
func (t Trigger) ToString() string {
builder := strings.Builder{}
for i, e := range t.events {
eventName := e.event
if strings.HasPrefix(eventName, "htmx:") {
eventName = eventName[5:]
}
builder.WriteString(eventName)
for _, m := range e.modifiers {
builder.WriteString(" ")
builder.WriteString(m.Modifier())
}
if i < len(t.events)-1 {
builder.WriteString(", ")
}
}
return builder.String()
}
func (t Trigger) Render(builder *strings.Builder) {
builder.WriteString(t.ToString())
}

16
framework/hx/triggers.go Normal file
View file

@ -0,0 +1,16 @@
package hx
// TriggerClick Common trigger events
const TriggerClick = ClickEvent
const TriggerClickOnce = TriggerClick + " once"
const TriggerDblClick = DblClickEvent
const TriggerKeyUpEnter = "keyup[keyCode==13]"
const TriggerEnterPressed = TriggerKeyUpEnter
const TriggerBlur = "blur"
const TriggerEvery1s = "every:1s"
const TriggerEvery2s = "every:2s"
const TriggerEvery5s = "every:5s"
const TriggerEvery10s = "every:10s"
const TriggerEvery30s = "every:30s"
const TriggerEvery1m = "every:1m"
const TriggerLoad = LoadEvent

View file

@ -3,22 +3,22 @@ module htmgo-site
go 1.23.0 go 1.23.0
require ( require (
github.com/google/uuid v1.6.0
github.com/labstack/echo/v4 v4.12.0 github.com/labstack/echo/v4 v4.12.0
github.com/maddalax/htmgo/framework v0.0.0-20240920021308-279a3c716342 github.com/maddalax/htmgo/framework v0.0.0-20240920021308-279a3c716342
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/yuin/goldmark v1.7.4
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
) )
require ( require (
github.com/alecthomas/chroma/v2 v2.2.0 // indirect github.com/alecthomas/chroma/v2 v2.2.0 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/yuin/goldmark v1.7.4 // indirect
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect
golang.org/x/crypto v0.27.0 // indirect golang.org/x/crypto v0.27.0 // indirect
golang.org/x/net v0.29.0 // indirect golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect golang.org/x/sys v0.25.0 // indirect

View file

@ -1,5 +1,6 @@
github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY= github.com/alecthomas/chroma/v2 v2.2.0 h1:Aten8jfQwUqEdadVFFjNyjx7HTexhKP0XuqBG67mRDY=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae h1:zzGwJfFlFGD94CyyYwCJeSuD32Gj9GTaSi5y9hoVzdY=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

View file

@ -4,8 +4,10 @@ import (
"embed" "embed"
"github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/htmgo/service" "github.com/maddalax/htmgo/framework/htmgo/service"
"github.com/maddalax/htmgo/framework/hx"
"htmgo-site/internal/markdown" "htmgo-site/internal/markdown"
"htmgo-site/pages/base" "htmgo-site/pages/base"
"htmgo-site/partials"
) )
func MarkdownHandler(ctx *h.RequestContext, path string) error { func MarkdownHandler(ctx *h.RequestContext, path string) error {
@ -15,6 +17,9 @@ func MarkdownHandler(ctx *h.RequestContext, path string) error {
func MarkdownPage(ctx *h.RequestContext, path string) *h.Element { func MarkdownPage(ctx *h.RequestContext, path string) *h.Element {
return base.RootPage( return base.RootPage(
h.Div( h.Div(
h.Div(
h.GetPartial(partials.TestPartial, hx.LoadEvent),
),
h.Class("w-full p-4 flex flex-col justify-center items-center"), h.Class("w-full p-4 flex flex-col justify-center items-center"),
MarkdownContent(ctx, path), MarkdownContent(ctx, path),
h.Div( h.Div(

View file

@ -11,6 +11,10 @@ func GetPartialFromContext(ctx echo.Context) *h.Partial {
cc := ctx.(*h.RequestContext) cc := ctx.(*h.RequestContext)
return partials.ToggleNavbar(cc) return partials.ToggleNavbar(cc)
} }
if path == "TestPartial" || path == "/htmgo-site/partials.TestPartial" {
cc := ctx.(*h.RequestContext)
return partials.TestPartial(cc)
}
return nil return nil
} }

View file

@ -1,6 +1,8 @@
package partials package partials
import "github.com/maddalax/htmgo/framework/h" import (
"github.com/maddalax/htmgo/framework/h"
)
type NavItem struct { type NavItem struct {
Name string Name string
@ -13,6 +15,12 @@ func ToggleNavbar(ctx *h.RequestContext) *h.Partial {
) )
} }
func TestPartial(ctx *h.RequestContext) *h.Partial {
return h.NewPartial(
h.Div(h.Text("This is a test")),
)
}
var navItems = []NavItem{ var navItems = []NavItem{
{Name: "Docs", Url: "/docs"}, {Name: "Docs", Url: "/docs"},
{Name: "Examples", Url: "/examples"}, {Name: "Examples", Url: "/examples"},
@ -37,6 +45,7 @@ func NavBar(expanded bool) *h.Element {
) )
desktopNav := h.Nav( desktopNav := h.Nav(
h.Script("https://buttons.github.io/buttons.js"),
h.Class("hidden sm:block bg-neutral-100 border border-b-slate-300 p-4 md:p-3"), h.Class("hidden sm:block bg-neutral-100 border border-b-slate-300 p-4 md:p-3"),
h.Div( h.Div(
h.Class("max-w-[95%] md:max-w-prose mx-auto"), h.Class("max-w-[95%] md:max-w-prose mx-auto"),
@ -45,6 +54,7 @@ func NavBar(expanded bool) *h.Element {
h.Div( h.Div(
h.Class("flex items-center"), h.Class("flex items-center"),
h.A( h.A(
h.Boost(),
h.Class("text-2xl"), h.Class("text-2xl"),
h.Href("/"), h.Href("/"),
h.Text("htmgo"), h.Text("htmgo"),
@ -55,6 +65,7 @@ func NavBar(expanded bool) *h.Element {
return h.Div( return h.Div(
h.Class("flex items-center"), h.Class("flex items-center"),
h.A( h.A(
h.Boost(),
h.Class(""), h.Class(""),
h.Href(item.Url), h.Href(item.Url),
h.Text(item.Name), h.Text(item.Name),
@ -87,6 +98,7 @@ func MobileNav(expanded bool) *h.Element {
h.Div( h.Div(
h.Class("flex items-center"), h.Class("flex items-center"),
h.A( h.A(
h.Boost(),
h.Class("text-2xl"), h.Class("text-2xl"),
h.Href("/"), h.Href("/"),
h.Text("htmgo"), h.Text("htmgo"),
@ -94,8 +106,19 @@ func MobileNav(expanded bool) *h.Element {
h.Div( h.Div(
h.Class("flex items-center"), h.Class("flex items-center"),
h.Button( h.Button(
h.GetPartialWithQs(ToggleNavbar, h.Ternary(expanded, "expanded=false", "expanded=true")), h.Boost(),
h.Trigger(h.TriggerClick),
h.GetPartialWithQs(
ToggleNavbar,
h.NewQs("expanded", h.Ternary(expanded, "false", "true"), "test", "true"),
"click",
),
h.AttributePairs(
"class", "text-2xl",
"aria-expanded", h.Ternary(expanded, "true", "false"),
),
h.Class("text-2xl"), h.Class("text-2xl"),
h.Text("☰"), h.Text("☰"),
), ),
@ -109,6 +132,7 @@ func MobileNav(expanded bool) *h.Element {
return h.Div( return h.Div(
h.Class("flex items-center"), h.Class("flex items-center"),
h.A( h.A(
h.Boost(),
h.Class(""), h.Class(""),
h.Href(item.Url), h.Href(item.Url),
h.Text(item.Name), h.Text(item.Name),