more cleanup
This commit is contained in:
parent
c555da4dc9
commit
da7e22c446
16 changed files with 444 additions and 344 deletions
|
|
@ -47,10 +47,10 @@ func Button(props ButtonProps) h.Ren {
|
||||||
button := h.Button(
|
button := h.Button(
|
||||||
h.If(props.Id != "", h.Id(props.Id)),
|
h.If(props.Id != "", h.Id(props.Id)),
|
||||||
h.If(props.Children != nil, h.Children(props.Children...)),
|
h.If(props.Children != nil, h.Children(props.Children...)),
|
||||||
h.If(props.Trigger != "", h.Trigger(props.Trigger)),
|
h.If(props.Trigger != "", h.HxTrigger(props.Trigger)),
|
||||||
h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class),
|
h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class),
|
||||||
h.If(props.Get != "", h.Get(props.Get)),
|
h.If(props.Get != "", h.Get(props.Get)),
|
||||||
h.If(props.Target != "", h.Target(props.Target)),
|
h.If(props.Target != "", h.HxTarget(props.Target)),
|
||||||
h.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")),
|
h.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")),
|
||||||
lifecycle,
|
lifecycle,
|
||||||
text,
|
text,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/maddalax/htmgo/framework/htmgo/service"
|
"github.com/maddalax/htmgo/framework/htmgo/service"
|
||||||
|
"github.com/maddalax/htmgo/framework/hx"
|
||||||
"github.com/maddalax/htmgo/framework/util/process"
|
"github.com/maddalax/htmgo/framework/util/process"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -12,6 +13,13 @@ import (
|
||||||
type RequestContext struct {
|
type RequestContext struct {
|
||||||
echo.Context
|
echo.Context
|
||||||
locator *service.Locator
|
locator *service.Locator
|
||||||
|
isBoosted bool
|
||||||
|
currentBrowserUrl string
|
||||||
|
hxPromptResponse string
|
||||||
|
isHxRequest bool
|
||||||
|
hxTargetId string
|
||||||
|
hxTriggerName string
|
||||||
|
hxTriggerId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RequestContext) ServiceLocator() *service.Locator {
|
func (c *RequestContext) ServiceLocator() *service.Locator {
|
||||||
|
|
@ -47,6 +55,16 @@ func Start(opts AppOpts) {
|
||||||
instance.start()
|
instance.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func populateHxFields(cc *RequestContext) {
|
||||||
|
cc.isBoosted = cc.Request().Header.Get(hx.BoostedHeader) == "true"
|
||||||
|
cc.currentBrowserUrl = cc.Request().Header.Get(hx.CurrentUrlHeader)
|
||||||
|
cc.hxPromptResponse = cc.Request().Header.Get(hx.PromptResponseHeader)
|
||||||
|
cc.isHxRequest = cc.Request().Header.Get(hx.RequestHeader) == "true"
|
||||||
|
cc.hxTargetId = cc.Request().Header.Get(hx.TargetIdHeader)
|
||||||
|
cc.hxTriggerName = cc.Request().Header.Get(hx.TriggerNameHeader)
|
||||||
|
cc.hxTriggerId = cc.Request().Header.Get(hx.TriggerIdHeader)
|
||||||
|
}
|
||||||
|
|
||||||
func (a App) start() {
|
func (a App) start() {
|
||||||
|
|
||||||
if a.Opts.Register != nil {
|
if a.Opts.Register != nil {
|
||||||
|
|
@ -56,9 +74,10 @@ func (a App) start() {
|
||||||
a.Echo.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
a.Echo.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
cc := &RequestContext{
|
cc := &RequestContext{
|
||||||
c,
|
Context: c,
|
||||||
a.Opts.ServiceLocator,
|
locator: a.Opts.ServiceLocator,
|
||||||
}
|
}
|
||||||
|
populateHxFields(cc)
|
||||||
return next(cc)
|
return next(cc)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package h
|
package h
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/maddalax/htmgo/framework/hx"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type AttributeMap map[string]any
|
type AttributeMap map[string]any
|
||||||
|
|
||||||
|
|
@ -21,3 +25,143 @@ func (m *AttributeMap) ToMap() map[string]string {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Attribute(key string, value string) *AttributeMap {
|
||||||
|
return Attributes(&AttributeMap{key: value})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AttributeList(children ...*AttributeMap) *AttributeMap {
|
||||||
|
m := make(AttributeMap)
|
||||||
|
for _, child := range children {
|
||||||
|
for k, v := range *child {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &m
|
||||||
|
}
|
||||||
|
|
||||||
|
func Attributes(attrs *AttributeMap) *AttributeMap {
|
||||||
|
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 {
|
||||||
|
return Attribute("checked", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Id(value string) Ren {
|
||||||
|
if strings.HasPrefix(value, "#") {
|
||||||
|
value = value[1:]
|
||||||
|
}
|
||||||
|
return Attribute("id", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Disabled() Ren {
|
||||||
|
return Attribute("disabled", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func HxTarget(target string) Ren {
|
||||||
|
return Attribute(hx.TargetAttr, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Name(name string) Ren {
|
||||||
|
return Attribute("name", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HxConfirm(message string) Ren {
|
||||||
|
return Attribute(hx.ConfirmAttr, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HxInclude https://htmx.org/attributes/hx-include/
|
||||||
|
func HxInclude(selector string) Ren {
|
||||||
|
return Attribute(hx.IncludeAttr, selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HxIndicator(tag string) *AttributeMap {
|
||||||
|
return Attribute(hx.IndicatorAttr, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TriggerChildren() Ren {
|
||||||
|
return HxExtension("trigger-children")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TriggerString(triggers ...string) *AttributeMap {
|
||||||
|
trigger := hx.NewStringTrigger(strings.Join(triggers, ", "))
|
||||||
|
return Attribute(hx.TriggerAttr, trigger.ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
func HxTrigger(opts ...hx.TriggerEvent) *AttributeMap {
|
||||||
|
return Attribute(hx.TriggerAttr, hx.NewTrigger(opts...).ToString())
|
||||||
|
}
|
||||||
|
|
||||||
|
func HxTriggerClick(opts ...hx.Modifier) *AttributeMap {
|
||||||
|
return HxTrigger(hx.OnClick(opts...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func HxExtension(value string) Ren {
|
||||||
|
return Attribute(hx.ExtAttr, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Href(path string) Ren {
|
||||||
|
return Attribute("href", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Type(name string) Ren {
|
||||||
|
return Attribute("type", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Placeholder(placeholder string) Ren {
|
||||||
|
return Attribute("placeholder", placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hidden() Ren {
|
||||||
|
return Attribute("style", "display:none")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Class(value ...string) Ren {
|
||||||
|
return Attribute("class", MergeClasses(value...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClassX(value string, m ClassMap) Ren {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
builder.WriteString(value)
|
||||||
|
builder.WriteString(" ")
|
||||||
|
for k, v := range m {
|
||||||
|
if v {
|
||||||
|
builder.WriteString(k)
|
||||||
|
builder.WriteString(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Class(builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func MergeClasses(classes ...string) string {
|
||||||
|
if len(classes) == 1 {
|
||||||
|
return classes[0]
|
||||||
|
}
|
||||||
|
builder := strings.Builder{}
|
||||||
|
for _, s := range classes {
|
||||||
|
builder.WriteString(s)
|
||||||
|
builder.WriteString(" ")
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Boost() Ren {
|
||||||
|
return Attribute(hx.BoostAttr, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
func IfQueryParam(key string, node *Element) Ren {
|
||||||
|
return Fragment(Attribute("hx-if-qp:"+key, "true"), node)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,18 @@ func NewPartial(root *Element) *Partial {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SwapManyPartial(ctx *RequestContext, swaps ...*Element) *Partial {
|
||||||
|
return NewPartial(
|
||||||
|
SwapMany(ctx, swaps...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SwapManyXPartial(ctx *RequestContext, swaps ...SwapArg) *Partial {
|
||||||
|
return NewPartial(
|
||||||
|
SwapManyX(ctx, swaps...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func GetPartialPath(partial func(ctx *RequestContext) *Partial) string {
|
func GetPartialPath(partial func(ctx *RequestContext) *Partial) string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(partial).Pointer()).Name()
|
return runtime.FuncForPC(reflect.ValueOf(partial).Pointer()).Name()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@ func If(condition bool, node Ren) Ren {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Ternary[T any](value bool, a T, b T) T {
|
||||||
|
if value {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func IfElse(condition bool, node Ren, node2 Ren) Ren {
|
func IfElse(condition bool, node Ren, node2 Ren) Ren {
|
||||||
if condition {
|
if condition {
|
||||||
return node
|
return node
|
||||||
|
|
@ -30,3 +37,10 @@ func IfHtmxRequest(ctx *RequestContext, node Ren) Ren {
|
||||||
}
|
}
|
||||||
return Empty()
|
return Empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClassIf(condition bool, value string) Ren {
|
||||||
|
if condition {
|
||||||
|
return Class(value)
|
||||||
|
}
|
||||||
|
return Empty()
|
||||||
|
}
|
||||||
|
|
|
||||||
41
framework/h/header.go
Normal file
41
framework/h/header.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package h
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/maddalax/htmgo/framework/hx"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReplaceUrlHeader(url string) *Headers {
|
||||||
|
return NewHeaders(hx.ReplaceUrlHeader, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CombineHeaders(headers ...*Headers) *Headers {
|
||||||
|
m := make(Headers)
|
||||||
|
for _, h := range headers {
|
||||||
|
for k, v := range *h {
|
||||||
|
m[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &m
|
||||||
|
}
|
||||||
|
|
||||||
|
func CurrentPath(ctx *RequestContext) string {
|
||||||
|
current := ctx.Request().Header.Get(hx.CurrentUrlHeader)
|
||||||
|
parsed, err := url.Parse(current)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parsed.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHeaders(headers ...string) *Headers {
|
||||||
|
if len(headers)%2 != 0 {
|
||||||
|
return &Headers{}
|
||||||
|
}
|
||||||
|
m := make(Headers)
|
||||||
|
for i := 0; i < len(headers); i++ {
|
||||||
|
m[headers[i]] = headers[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return &m
|
||||||
|
}
|
||||||
89
framework/h/qs.go
Normal file
89
framework/h/qs.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package h
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/maddalax/htmgo/framework/hx"
|
||||||
|
"net/url"
|
||||||
|
"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) Remove(key string) *Qs {
|
||||||
|
delete(q.m, key)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func PushQsHeader(ctx *RequestContext, qs *Qs) *Headers {
|
||||||
|
parsed, err := url.Parse(ctx.currentBrowserUrl)
|
||||||
|
if err != nil {
|
||||||
|
return NewHeaders()
|
||||||
|
}
|
||||||
|
return NewHeaders(hx.ReplaceUrlHeader, SetQueryParams(parsed.Path, qs))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQueryParam(ctx *RequestContext, key string) string {
|
||||||
|
value := ctx.QueryParam(key)
|
||||||
|
if value == "" {
|
||||||
|
current := ctx.currentBrowserUrl
|
||||||
|
if current != "" {
|
||||||
|
u, err := url.Parse(current)
|
||||||
|
if err == nil {
|
||||||
|
return u.Query().Get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetQueryParams(href string, qs *Qs) string {
|
||||||
|
u, err := url.Parse(href)
|
||||||
|
if err != nil {
|
||||||
|
return href
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
for key, value := range qs.m {
|
||||||
|
if value == "" {
|
||||||
|
q.Del(key)
|
||||||
|
} else {
|
||||||
|
q.Set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Ren interface {
|
||||||
|
Render(builder *strings.Builder)
|
||||||
|
}
|
||||||
|
|
||||||
func Render(node Ren) string {
|
func Render(node Ren) string {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
builder := &strings.Builder{}
|
builder := &strings.Builder{}
|
||||||
|
|
|
||||||
83
framework/h/swap.go
Normal file
83
framework/h/swap.go
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package h
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/maddalax/htmgo/framework/hx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SwapArg struct {
|
||||||
|
Content *Element
|
||||||
|
Option SwapOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwapOption struct {
|
||||||
|
Selector string
|
||||||
|
SwapType hx.SwapType
|
||||||
|
Modifier string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSwap(content *Element, opts ...SwapOption) SwapArg {
|
||||||
|
option := SwapOption{}
|
||||||
|
if len(opts) > 0 {
|
||||||
|
option = opts[0]
|
||||||
|
}
|
||||||
|
return SwapArg{
|
||||||
|
Content: content,
|
||||||
|
Option: option,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func OobSwap(ctx *RequestContext, content *Element, option ...SwapOption) *Element {
|
||||||
|
return OobSwapWithSelector(ctx, "", content, option...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OobSwapWithSelector(ctx *RequestContext, selector string, content *Element, option ...SwapOption) *Element {
|
||||||
|
if ctx == nil || !ctx.isHxRequest {
|
||||||
|
return Empty()
|
||||||
|
}
|
||||||
|
return content.AppendChild(outOfBandSwap(selector, option...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func outOfBandSwap(selector string, option ...SwapOption) Ren {
|
||||||
|
swapType := hx.SwapTypeTrue
|
||||||
|
|
||||||
|
if len(option) > 0 {
|
||||||
|
o := option[0]
|
||||||
|
|
||||||
|
if o.SwapType != "" {
|
||||||
|
swapType = o.SwapType
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier := o.Modifier
|
||||||
|
if modifier != "" {
|
||||||
|
swapType = fmt.Sprintf("%s %s", swapType, modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Attribute(hx.SwapOobAttr,
|
||||||
|
Ternary(selector == "", swapType, selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
func SwapMany(ctx *RequestContext, elements ...*Element) *Element {
|
||||||
|
if !ctx.isHxRequest {
|
||||||
|
return Empty()
|
||||||
|
}
|
||||||
|
for _, element := range elements {
|
||||||
|
element.AppendChild(outOfBandSwap(""))
|
||||||
|
}
|
||||||
|
return Template(Map(elements, func(arg *Element) Ren {
|
||||||
|
return arg
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SwapManyX(ctx *RequestContext, args ...SwapArg) *Element {
|
||||||
|
if !ctx.isHxRequest {
|
||||||
|
return Empty()
|
||||||
|
}
|
||||||
|
for _, arg := range args {
|
||||||
|
arg.Content.AppendChild(outOfBandSwap("", arg.Option))
|
||||||
|
}
|
||||||
|
return Template(Map(args, func(arg SwapArg) Ren {
|
||||||
|
return arg.Content
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
@ -1,50 +1,10 @@
|
||||||
package h
|
package h
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/maddalax/htmgo/framework/hx"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Qs struct {
|
type ClassMap map[string]bool
|
||||||
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 PartialFunc = func(ctx *RequestContext) *Partial
|
||||||
|
|
||||||
|
|
@ -59,113 +19,7 @@ func (node *Element) AppendChild(child Ren) *Element {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
func Data(data map[string]any) Ren {
|
func TextF(format string, args ...interface{}) *TextContent {
|
||||||
serialized, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return Empty()
|
|
||||||
}
|
|
||||||
return Attribute("x-data", string(serialized))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClassIf(condition bool, value string) Ren {
|
|
||||||
if condition {
|
|
||||||
return Class(value)
|
|
||||||
}
|
|
||||||
return Empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Class(value ...string) Ren {
|
|
||||||
return Attribute("class", MergeClasses(value...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClassX(value string, m ClassMap) Ren {
|
|
||||||
builder := strings.Builder{}
|
|
||||||
builder.WriteString(value)
|
|
||||||
builder.WriteString(" ")
|
|
||||||
for k, v := range m {
|
|
||||||
if v {
|
|
||||||
builder.WriteString(k)
|
|
||||||
builder.WriteString(" ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Class(builder.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeClasses(classes ...string) string {
|
|
||||||
if len(classes) == 1 {
|
|
||||||
return classes[0]
|
|
||||||
}
|
|
||||||
builder := strings.Builder{}
|
|
||||||
for _, s := range classes {
|
|
||||||
builder.WriteString(s)
|
|
||||||
builder.WriteString(" ")
|
|
||||||
}
|
|
||||||
return builder.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Id(value string) Ren {
|
|
||||||
if strings.HasPrefix(value, "#") {
|
|
||||||
value = value[1:]
|
|
||||||
}
|
|
||||||
return Attribute("id", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClassMap map[string]bool
|
|
||||||
|
|
||||||
func Attributes(attrs *AttributeMap) *AttributeMap {
|
|
||||||
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 {
|
|
||||||
return Attribute("checked", "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Boost() Ren {
|
|
||||||
return Attribute(hx.BoostAttr, "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Attribute(key string, value string) *AttributeMap {
|
|
||||||
return Attributes(&AttributeMap{key: value})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TriggerChildren() Ren {
|
|
||||||
return HxExtension("trigger-children")
|
|
||||||
}
|
|
||||||
|
|
||||||
func HxExtension(value string) Ren {
|
|
||||||
return Attribute(hx.ExtAttr, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Disabled() Ren {
|
|
||||||
return Attribute("disabled", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TriggerString(triggers ...string) *AttributeMap {
|
|
||||||
trigger := hx.NewStringTrigger(strings.Join(triggers, ", "))
|
|
||||||
return Attribute(hx.TriggerAttr, trigger.ToString())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Trigger(opts ...hx.TriggerEvent) *AttributeMap {
|
|
||||||
return Attribute(hx.TriggerAttr, hx.NewTrigger(opts...).ToString())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TriggerClick(opts ...hx.Modifier) *AttributeMap {
|
|
||||||
return Trigger(hx.OnClick(opts...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TextF(format string, args ...interface{}) Ren {
|
|
||||||
return Text(fmt.Sprintf(format, args...))
|
return Text(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,39 +31,6 @@ func Pf(format string, args ...interface{}) Ren {
|
||||||
return P(Text(fmt.Sprintf(format, args...)))
|
return P(Text(fmt.Sprintf(format, args...)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Target(target string) Ren {
|
|
||||||
return Attribute(hx.TargetAttr, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Name(name string) Ren {
|
|
||||||
return Attribute("name", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Confirm(message string) Ren {
|
|
||||||
return Attribute(hx.ConfirmAttr, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Href(path string) Ren {
|
|
||||||
return Attribute("href", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type(name string) Ren {
|
|
||||||
return Attribute("type", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Placeholder(placeholder string) Ren {
|
|
||||||
return Attribute("placeholder", placeholder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func OutOfBandSwap(selector string) Ren {
|
|
||||||
return Attribute(hx.SwapOobAttr,
|
|
||||||
Ternary(selector == "", "true", selector))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Click(value string) Ren {
|
|
||||||
return Attribute("onclick", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Tag(tag string, children ...Ren) *Element {
|
func Tag(tag string, children ...Ren) *Element {
|
||||||
return &Element{
|
return &Element{
|
||||||
tag: tag,
|
tag: tag,
|
||||||
|
|
@ -292,52 +113,6 @@ func Article(children ...Ren) *Element {
|
||||||
return Tag("article", children...)
|
return Tag("article", children...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReplaceUrlHeader(url string) *Headers {
|
|
||||||
return NewHeaders(hx.ReplaceUrlHeader, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CombineHeaders(headers ...*Headers) *Headers {
|
|
||||||
m := make(Headers)
|
|
||||||
for _, h := range headers {
|
|
||||||
for k, v := range *h {
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &m
|
|
||||||
}
|
|
||||||
|
|
||||||
func CurrentPath(ctx *RequestContext) string {
|
|
||||||
current := ctx.Request().Header.Get(hx.CurrentUrlHeader)
|
|
||||||
parsed, err := url.Parse(current)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return parsed.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
func PushQsHeader(ctx *RequestContext, key string, value string) *Headers {
|
|
||||||
current := ctx.Request().Header.Get(hx.CurrentUrlHeader)
|
|
||||||
parsed, err := url.Parse(current)
|
|
||||||
if err != nil {
|
|
||||||
return NewHeaders()
|
|
||||||
}
|
|
||||||
return NewHeaders(hx.ReplaceUrlHeader, SetQueryParams(parsed.Path, map[string]string{
|
|
||||||
key: value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHeaders(headers ...string) *Headers {
|
|
||||||
if len(headers)%2 != 0 {
|
|
||||||
return &Headers{}
|
|
||||||
}
|
|
||||||
m := make(Headers)
|
|
||||||
for i := 0; i < len(headers); i++ {
|
|
||||||
m[headers[i]] = headers[i+1]
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return &m
|
|
||||||
}
|
|
||||||
|
|
||||||
func Checkbox(children ...Ren) Ren {
|
func Checkbox(children ...Ren) Ren {
|
||||||
return Input("checkbox", children...)
|
return Input("checkbox", children...)
|
||||||
}
|
}
|
||||||
|
|
@ -368,14 +143,8 @@ func Fragment(children ...Ren) *ChildList {
|
||||||
return Children(children...)
|
return Children(children...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AttributeList(children ...*AttributeMap) *AttributeMap {
|
func Template(children ...Ren) *Element {
|
||||||
m := make(AttributeMap)
|
return Tag("template", children...)
|
||||||
for _, child := range children {
|
|
||||||
for k, v := range *child {
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func AppendChildren(node *Element, children ...Ren) Ren {
|
func AppendChildren(node *Element, children ...Ren) Ren {
|
||||||
|
|
@ -388,10 +157,6 @@ func Button(children ...Ren) *Element {
|
||||||
return Tag("button", children...)
|
return Tag("button", children...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Indicator(tag string) *AttributeMap {
|
|
||||||
return Attribute(hx.IndicatorAttr, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func P(children ...Ren) *Element {
|
func P(children ...Ren) *Element {
|
||||||
return Tag("p", children...)
|
return Tag("p", children...)
|
||||||
}
|
}
|
||||||
|
|
@ -446,14 +211,6 @@ func Empty() *Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func IfQueryParam(key string, node *Element) Ren {
|
|
||||||
return Fragment(Attribute("hx-if-qp:"+key, "true"), node)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hidden() Ren {
|
|
||||||
return Attribute("style", "display:none")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Children(children ...Ren) *ChildList {
|
func Children(children ...Ren) *ChildList {
|
||||||
return NewChildList(children...)
|
return NewChildList(children...)
|
||||||
}
|
}
|
||||||
|
|
@ -461,42 +218,3 @@ func Children(children ...Ren) *ChildList {
|
||||||
func Label(text string) *Element {
|
func Label(text string) *Element {
|
||||||
return Tag("label", Text(text))
|
return Tag("label", Text(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTriggerName(ctx *RequestContext) string {
|
|
||||||
return ctx.Request().Header.Get("HX-Trigger-Name")
|
|
||||||
}
|
|
||||||
|
|
||||||
type SwapArg struct {
|
|
||||||
Selector string
|
|
||||||
Content *Element
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSwap(selector string, content *Element) SwapArg {
|
|
||||||
return SwapArg{
|
|
||||||
Selector: selector,
|
|
||||||
Content: content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func OobSwap(ctx *RequestContext, content *Element) *Element {
|
|
||||||
return OobSwapWithSelector(ctx, "", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func OobSwapWithSelector(ctx *RequestContext, selector string, content *Element) *Element {
|
|
||||||
if ctx == nil || ctx.Get("HX-Request") == "" {
|
|
||||||
return Empty()
|
|
||||||
}
|
|
||||||
return content.AppendChild(OutOfBandSwap(selector))
|
|
||||||
}
|
|
||||||
|
|
||||||
func SwapMany(ctx *RequestContext, args ...SwapArg) Ren {
|
|
||||||
if ctx.Get("HX-Request") == "" {
|
|
||||||
return Empty()
|
|
||||||
}
|
|
||||||
for _, arg := range args {
|
|
||||||
arg.Content.AppendChild(OutOfBandSwap(arg.Selector))
|
|
||||||
}
|
|
||||||
return Fragment(Map(args, func(arg SwapArg) Ren {
|
|
||||||
return arg.Content
|
|
||||||
})...)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,8 @@ package h
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Ren interface {
|
|
||||||
Render(builder *strings.Builder)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ternary[T any](value bool, a T, b T) T {
|
|
||||||
if value {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func JsonSerialize(data any) string {
|
func JsonSerialize(data any) string {
|
||||||
serialized, err := json.Marshal(data)
|
serialized, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -25,34 +11,3 @@ func JsonSerialize(data any) string {
|
||||||
}
|
}
|
||||||
return string(serialized)
|
return string(serialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetQueryParam(ctx echo.Context, key string) string {
|
|
||||||
value := ctx.QueryParam(key)
|
|
||||||
if value == "" {
|
|
||||||
current := ctx.Request().Header.Get("Hx-Current-Url")
|
|
||||||
if current != "" {
|
|
||||||
u, err := url.Parse(current)
|
|
||||||
if err == nil {
|
|
||||||
return u.Query().Get(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetQueryParams(href string, qs map[string]string) string {
|
|
||||||
u, err := url.Parse(href)
|
|
||||||
if err != nil {
|
|
||||||
return href
|
|
||||||
}
|
|
||||||
q := u.Query()
|
|
||||||
for key, value := range qs {
|
|
||||||
if value == "" {
|
|
||||||
q.Del(key)
|
|
||||||
} else {
|
|
||||||
q.Set(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
u.RawQuery = q.Encode()
|
|
||||||
return u.String()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ func GetPartialWithQs(partial PartialFunc, qs *Qs, trigger string) *AttributeMap
|
||||||
return Get(GetPartialPathWithQs(partial, qs), trigger)
|
return Get(GetPartialPathWithQs(partial, qs), trigger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetWithQs(path string, qs map[string]string, trigger string) *AttributeMap {
|
func GetWithQs(path string, qs *Qs, trigger string) *AttributeMap {
|
||||||
return Get(SetQueryParams(path, qs), trigger)
|
return Get(SetQueryParams(path, qs), trigger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package hx
|
||||||
type Attribute = string
|
type Attribute = string
|
||||||
type Header = string
|
type Header = string
|
||||||
type Event = string
|
type Event = string
|
||||||
|
type SwapType = string
|
||||||
|
|
||||||
// https://htmx.org/reference/#events
|
// https://htmx.org/reference/#events
|
||||||
const (
|
const (
|
||||||
|
|
@ -42,6 +43,12 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
BoostedHeader Header = "HX-Boosted"
|
||||||
|
PromptResponseHeader Header = "HX-Prompt"
|
||||||
|
RequestHeader Header = "HX-Request"
|
||||||
|
TargetIdHeader Header = "HX-Target"
|
||||||
|
TriggerNameHeader Header = "HX-Trigger-Name"
|
||||||
|
TriggerIdHeader Header = "HX-Trigger"
|
||||||
LocationHeader Header = "HX-Location"
|
LocationHeader Header = "HX-Location"
|
||||||
PushUrlHeader Header = "HX-Push-Url"
|
PushUrlHeader Header = "HX-Push-Url"
|
||||||
RedirectHeader Header = "HX-Redirect"
|
RedirectHeader Header = "HX-Redirect"
|
||||||
|
|
@ -136,3 +143,16 @@ const (
|
||||||
DropEvent Event = "ondrop"
|
DropEvent Event = "ondrop"
|
||||||
DragEndEvent Event = "ondragend"
|
DragEndEvent Event = "ondragend"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SwapTypeTrue SwapType = "true"
|
||||||
|
SwapTypeInnerHtml SwapType = "innerHTML"
|
||||||
|
SwapTypeOuterHtml SwapType = "outerHTML"
|
||||||
|
SwapTypeTextContent SwapType = "textContent"
|
||||||
|
SwapTypeBeforeBegin SwapType = "beforebegin"
|
||||||
|
SwapTypeAfterBegin SwapType = "afterbegin"
|
||||||
|
SwapTypeBeforeEnd SwapType = "beforeend"
|
||||||
|
SwapTypeAfterEnd SwapType = "afterend"
|
||||||
|
SwapTypeDelete SwapType = "delete"
|
||||||
|
SwapTypeNone SwapType = "none"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ type NavItem struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToggleNavbar(ctx *h.RequestContext) *h.Partial {
|
func ToggleNavbar(ctx *h.RequestContext) *h.Partial {
|
||||||
return h.NewPartial(
|
return h.SwapManyPartial(
|
||||||
h.OobSwap(ctx, MobileNav(h.GetQueryParam(ctx, "expanded") == "true")),
|
ctx,
|
||||||
|
MobileNav(h.GetQueryParam(ctx, "expanded") == "true"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func Create(ctx echo.Context) *h.Partial {
|
||||||
}
|
}
|
||||||
|
|
||||||
headers := h.CombineHeaders(h.PushQsHeader(ctx, "adding", ""), &map[string]string{
|
headers := h.CombineHeaders(h.PushQsHeader(ctx, "adding", ""), &map[string]string{
|
||||||
"HX-Trigger": "patient-added",
|
"HX-HxTrigger": "patient-added",
|
||||||
})
|
})
|
||||||
|
|
||||||
return h.NewPartialWithHeaders(
|
return h.NewPartialWithHeaders(
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ func Input(list []*ent.Task) *h.Element {
|
||||||
h.Class("pl-12 text-xl p-4 w-full outline-none focus:outline-2 focus:outline-rose-400"),
|
h.Class("pl-12 text-xl p-4 w-full outline-none focus:outline-2 focus:outline-rose-400"),
|
||||||
h.Placeholder("What needs to be done?"),
|
h.Placeholder("What needs to be done?"),
|
||||||
h.Post(h.GetPartialPath(Create)),
|
h.Post(h.GetPartialPath(Create)),
|
||||||
h.Trigger("keyup[keyCode==13]"),
|
h.HxTrigger("keyup[keyCode==13]"),
|
||||||
),
|
),
|
||||||
CompleteAllIcon(list),
|
CompleteAllIcon(list),
|
||||||
)
|
)
|
||||||
|
|
@ -162,7 +162,7 @@ func Task(task *ent.Task, editing bool) *h.Element {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
h.P(
|
h.P(
|
||||||
h.Trigger("dblclick"),
|
h.HxTrigger("dblclick"),
|
||||||
h.GetPartialWithQs(EditNameForm, "id="+task.ID.String()),
|
h.GetPartialWithQs(EditNameForm, "id="+task.ID.String()),
|
||||||
h.ClassX("text-xl break-all text-wrap truncate", map[string]bool{
|
h.ClassX("text-xl break-all text-wrap truncate", map[string]bool{
|
||||||
"line-through text-slate-400": task.CompletedAt != nil,
|
"line-through text-slate-400": task.CompletedAt != nil,
|
||||||
|
|
@ -174,7 +174,7 @@ func Task(task *ent.Task, editing bool) *h.Element {
|
||||||
|
|
||||||
func CompleteIcon(task *ent.Task) *h.Element {
|
func CompleteIcon(task *ent.Task) *h.Element {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Trigger("click"),
|
h.HxTrigger("click"),
|
||||||
h.Post(h.GetPartialPathWithQs(ToggleCompleted, "id="+task.ID.String())),
|
h.Post(h.GetPartialPathWithQs(ToggleCompleted, "id="+task.ID.String())),
|
||||||
h.Class("flex items-center justify-center cursor-pointer"),
|
h.Class("flex items-center justify-center cursor-pointer"),
|
||||||
h.Div(
|
h.Div(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue