diff --git a/framework-ui/ui/button.go b/framework-ui/ui/button.go index e76f21f..76efd60 100644 --- a/framework-ui/ui/button.go +++ b/framework-ui/ui/button.go @@ -10,20 +10,20 @@ type ButtonProps struct { Trigger string Get string Class string - Children []h.Renderable + Children []h.Ren } -func PrimaryButton(props ButtonProps) h.Renderable { +func PrimaryButton(props ButtonProps) h.Ren { props.Class = h.MergeClasses(props.Class, "border-blue-700 bg-blue-700 text-white") return Button(props) } -func SecondaryButton(props ButtonProps) h.Renderable { +func SecondaryButton(props ButtonProps) h.Ren { props.Class = h.MergeClasses(props.Class, "border-gray-700 bg-gray-700 text-white") return Button(props) } -func Button(props ButtonProps) h.Renderable { +func Button(props ButtonProps) h.Ren { text := h.Text(props.Text) diff --git a/framework-ui/ui/input.go b/framework-ui/ui/input.go index 85afb41..94fff5b 100644 --- a/framework-ui/ui/input.go +++ b/framework-ui/ui/input.go @@ -11,10 +11,10 @@ type InputProps struct { Type string DefaultValue string ValidationPath string - Children []h.Renderable + Children []h.Ren } -func Input(props InputProps) h.Renderable { +func Input(props InputProps) h.Ren { validation := h.If(props.ValidationPath != "", h.Children( h.Post(props.ValidationPath), h.Trigger("change"), diff --git a/framework/h/app.go b/framework/h/app.go index 6711941..667d2d2 100644 --- a/framework/h/app.go +++ b/framework/h/app.go @@ -88,7 +88,7 @@ func (a App) start() { } func HtmlView(c echo.Context, page *Page) error { - root := page.Root.Render() + root := page.Root return c.HTML(200, Render( root, @@ -111,7 +111,7 @@ func PartialViewWithHeaders(c echo.Context, headers *Headers, partial *Partial) return c.HTML(200, Render( - partial.Root, + partial, ), ) } @@ -126,7 +126,7 @@ func PartialView(c echo.Context, partial *Partial) error { return c.HTML(200, Render( - partial.Root, + partial, ), ) } diff --git a/framework/h/attribute.go b/framework/h/attribute.go new file mode 100644 index 0000000..1807f0a --- /dev/null +++ b/framework/h/attribute.go @@ -0,0 +1,43 @@ +package h + +type AttributeR struct { + Name string + Value string +} + +func NewAttribute(name string, value string) *AttributeR { + return &AttributeR{ + Name: name, + Value: value, + } +} + +type TextContent struct { + Content string +} + +func NewTextContent(content string) *TextContent { + return &TextContent{ + Content: content, + } +} + +type RawContent struct { + Content string +} + +func NewRawContent(content string) *RawContent { + return &RawContent{ + Content: content, + } +} + +type ChildList struct { + Children []Ren +} + +func NewChildList(children ...Ren) *ChildList { + return &ChildList{ + Children: children, + } +} diff --git a/framework/h/base.go b/framework/h/base.go index 73286b6..beb79a9 100644 --- a/framework/h/base.go +++ b/framework/h/base.go @@ -11,40 +11,40 @@ type Headers = map[string]string type Partial struct { Headers *Headers - Root *Node + Root string } -func (p *Partial) Render() *Node { +func (p *Partial) Render() string { return p.Root } type Page struct { - Root Renderable + Root Ren HttpMethod string } -func NewPage(root Renderable) *Page { +func NewPage(root Ren) *Page { return &Page{ HttpMethod: http.MethodGet, Root: root, } } -func NewPageWithHttpMethod(httpMethod string, root Renderable) *Page { +func NewPageWithHttpMethod(httpMethod string, root Ren) *Page { return &Page{ HttpMethod: httpMethod, Root: root, } } -func NewPartialWithHeaders(headers *Headers, root Renderable) *Partial { +func NewPartialWithHeaders(headers *Headers, root Ren) *Partial { return &Partial{ Headers: headers, Root: root.Render(), } } -func NewPartial(root Renderable) *Partial { +func NewPartial(root Ren) *Partial { return &Partial{ Root: root.Render(), } diff --git a/framework/h/events.go b/framework/h/events.go index 198aa82..457a010 100644 --- a/framework/h/events.go +++ b/framework/h/events.go @@ -1,6 +1,7 @@ package h type HxEvent = string +type HxTriggerName = string var ( HxBeforeRequest HxEvent = "hx-on::before-request" @@ -13,3 +14,11 @@ var ( 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" +) diff --git a/framework/h/lifecycle.go b/framework/h/lifecycle.go index 7b4e721..24beee7 100644 --- a/framework/h/lifecycle.go +++ b/framework/h/lifecycle.go @@ -53,25 +53,6 @@ func (l *LifeCycle) OnMutationError(cmd ...JsCommand) *LifeCycle { return l } -func (l *LifeCycle) Render() *Node { - m := make(map[string]string) - - for event, commands := range l.handlers { - m[event] = "" - for _, command := range commands { - m[event] += fmt.Sprintf("%s;", command.Command) - } - } - - children := make([]Renderable, 0) - - for event, js := range m { - children = append(children, Attribute(event, js)) - } - - return Children(children...).Render() -} - type JsCommand struct { Command string } @@ -86,14 +67,14 @@ func Increment(amount int) JsCommand { return JsCommand{Command: fmt.Sprintf("this.innerText = parseInt(this.innerText) + %d", amount)} } -func SetInnerHtml(r Renderable) JsCommand { +func SetInnerHtml(r Ren) JsCommand { // language=JavaScript - return JsCommand{Command: fmt.Sprintf("this.innerHTML = `%s`", Render(r.Render()))} + return JsCommand{Command: fmt.Sprintf("this.innerHTML = `%s`", r.Render())} } -func SetOuterHtml(r Renderable) JsCommand { +func SetOuterHtml(r Ren) JsCommand { // language=JavaScript - return JsCommand{Command: fmt.Sprintf("this.outerHTML = `%s`", Render(r.Render()))} + return JsCommand{Command: fmt.Sprintf("this.outerHTML = `%s`", r.Render())} } func AddAttribute(name, value string) JsCommand { diff --git a/framework/h/render.go b/framework/h/render.go index c8dcc15..ec4a140 100644 --- a/framework/h/render.go +++ b/framework/h/render.go @@ -2,151 +2,13 @@ package h import ( "fmt" - "strings" "time" ) -const FlagSkip = "skip" -const FlagText = "text" -const FlagRaw = "raw" -const FlagAttributeList = "x-attribute-list" -const FlagChildrenList = "x-children-list" - -type Builder struct { - builder *strings.Builder - root *Node -} - -func (page Builder) render() { - page.renderNode(page.root) -} - -func insertAttribute(node *Node, name string, value string) { - existing := node.attributes[name] - if existing != "" { - node.attributes[name] = existing + " " + value - } else { - node.attributes[name] = value - } -} - -func (page Builder) renderNode(node *Node) { - if node.tag != "" { - page.builder.WriteString(fmt.Sprintf("<%s", node.tag)) - index := 0 - - if node.attributes == nil { - node.attributes = map[string]string{} - } - - flatChildren := make([]Renderable, 0) - for _, child := range node.children { - - if child == nil { - continue - } - - c := child.Render() - - flatChildren = append(flatChildren, child) - if c.tag == FlagChildrenList { - for _, gc := range c.children { - flatChildren = append(flatChildren, gc) - } - c.tag = FlagSkip - } - } - - if len(flatChildren) > 0 { - node.children = flatChildren - } - - for _, child := range node.children { - - if child == nil { - continue - } - - c := child.Render() - - if c.tag == "class" { - insertAttribute(node, "class", c.value) - c.tag = FlagSkip - } - - if c.tag == FlagAttributeList { - for _, gc := range c.children { - gcr := gc.Render() - for key, value := range gcr.attributes { - insertAttribute(node, key, value) - } - gcr.tag = FlagSkip - } - c.tag = FlagSkip - } - - if c.tag == "attribute" { - for key, value := range c.attributes { - insertAttribute(node, key, value) - } - c.tag = FlagSkip - } - } - - for key, value := range node.attributes { - if index == 0 { - page.builder.WriteString(" ") - } - page.builder.WriteString(key) - page.builder.WriteString("=") - page.builder.WriteRune('"') - page.builder.WriteString(value) - page.builder.WriteRune('"') - if index < len(node.attributes) { - page.builder.WriteRune(' ') - } - index += 1 - } - page.builder.WriteString(">") - if node.text != "" { - page.builder.WriteString(node.text) - } - } - for _, child := range node.children { - - if child == nil { - continue - } - - c := child.Render() - - if c.tag == FlagText { - page.builder.WriteString(c.text) - continue - } - if c.tag == FlagRaw { - page.builder.WriteString(c.value) - continue - } - if c.tag != FlagSkip { - page.renderNode(c) - } - } - if node.tag != "" { - page.builder.WriteString(fmt.Sprintf("", node.tag)) - } -} - -func Render(node Renderable) string { +func Render(node Ren) string { start := time.Now() - builder := strings.Builder{} - page := Builder{ - builder: &builder, - root: node.Render(), - } - page.render() - d := page.builder.String() + html := node.Render() duration := time.Since(start) - fmt.Printf("render took %d\n", duration.Microseconds()) - return d + fmt.Printf("render took %d microseconds\n", duration.Microseconds()) + return html } diff --git a/framework/h/renderer.go b/framework/h/renderer.go new file mode 100644 index 0000000..732acee --- /dev/null +++ b/framework/h/renderer.go @@ -0,0 +1,137 @@ +package h + +import ( + "fmt" + "strings" +) + +func (node *Element) Render() string { + builder := &strings.Builder{} + + // some elements may not have a tag, such as a Fragment + if node.tag != "" { + builder.WriteString("<" + node.tag) + builder.WriteString(" ") + for name, value := range node.attributes { + builder.WriteString(NewAttribute(name, value).Render()) + } + } + + // first pass, flatten the children + flatChildren := make([]Ren, 0) + for _, child := range node.children { + switch child.(type) { + case *ChildList: + flatChildren = append(flatChildren, child.(*ChildList).Children...) + default: + flatChildren = append(flatChildren, child) + } + } + + node.children = flatChildren + + // second pass, render any attributes within the tag + for _, child := range node.children { + switch child.(type) { + case *AttributeMap: + builder.WriteString(child.(*AttributeMap).Render()) + case *LifeCycle: + builder.WriteString(child.(*LifeCycle).Render()) + } + } + + // close the tag + if node.tag != "" { + builder.WriteString(">") + } + + // render the children elements that are not attributes + for _, child := range node.children { + switch child.(type) { + case *AttributeMap: + continue + case *LifeCycle: + continue + default: + builder.WriteString(child.Render()) + } + } + + if node.tag != "" { + builder.WriteString("") + } + + str := builder.String() + return str +} + +func (a *AttributeR) Render() string { + return fmt.Sprintf(`%s="%s"`, a.Name, a.Value) +} + +func (t *TextContent) Render() string { + return t.Content +} + +func (r *RawContent) Render() string { + return r.Content +} + +func (c *ChildList) Render() string { + builder := &strings.Builder{} + for _, child := range c.Children { + builder.WriteString(child.Render()) + } + str := builder.String() + return str +} + +func (m *AttributeMap) Render() string { + builder := &strings.Builder{} + m2 := m.ToMap() + + for k, v := range m2 { + builder.WriteString(NewAttribute(k, v).Render()) + } + + str := builder.String() + return str +} + +func (l *LifeCycle) Render() string { + m := make(map[string]string) + + for event, commands := range l.handlers { + m[event] = "" + for _, command := range commands { + m[event] += fmt.Sprintf("%s;", command.Command) + } + } + + children := make([]Ren, 0) + + for event, js := range m { + children = append(children, Attribute(event, js)) + } + + result := Children(children...).Render() + return result +} + +func (m *AttributeMap) ToMap() map[string]string { + result := make(map[string]string) + for k, v := range *m { + switch v.(type) { + case AttributeMap: + m2 := v.(*AttributeMap).ToMap() + for _, a := range m2 { + result[k] = a + } + case string: + result[k] = v.(string) + default: + result[k] = fmt.Sprintf("%v", v) + } + } + return result +} diff --git a/framework/h/tag.go b/framework/h/tag.go index 3997879..21dce56 100644 --- a/framework/h/tag.go +++ b/framework/h/tag.go @@ -8,31 +8,18 @@ import ( "strings" ) -type Node struct { - id string +type Element struct { tag string attributes map[string]string - children []Renderable - text string - value string - changed bool + children []Ren } -func (node *Node) Render() *Node { - return node -} - -func (node *Node) AppendChild(child Renderable) Renderable { +func (node *Element) AppendChild(child Ren) Ren { node.children = append(node.children, child) return node } -func (node *Node) SetChanged(changed bool) Renderable { - node.changed = changed - return node -} - -func Data(data map[string]any) Renderable { +func Data(data map[string]any) Ren { serialized, err := json.Marshal(data) if err != nil { return Empty() @@ -40,86 +27,89 @@ func Data(data map[string]any) Renderable { return Attribute("x-data", string(serialized)) } -func ClassIf(condition bool, value string) Renderable { +func ClassIf(condition bool, value string) Ren { if condition { return Class(value) } return Empty() } -func Class(value ...string) Renderable { - return &Node{ - tag: "class", - value: MergeClasses(value...), - } +func Class(value ...string) Ren { + return Attribute("class", MergeClasses(value...)) } -func ClassX(value string, m map[string]bool) Renderable { +func ClassX(value string, m ClassMap) Ren { builder := strings.Builder{} - builder.WriteString(value + " ") + builder.WriteString(value) + builder.WriteString(" ") for k, v := range m { if v { - builder.WriteString(k + " ") + builder.WriteString(k) + builder.WriteString(" ") } } return Class(builder.String()) } func MergeClasses(classes ...string) string { - builder := "" - for _, s := range classes { - builder += s + " " + if len(classes) == 1 { + return classes[0] } - return builder + builder := strings.Builder{} + for _, s := range classes { + builder.WriteString(s) + builder.WriteString(" ") + } + return builder.String() } -func Id(value string) Renderable { +func Id(value string) Ren { if strings.HasPrefix(value, "#") { value = value[1:] } return Attribute("id", value) } -func Attributes(attrs map[string]string) Renderable { - return &Node{ - tag: "attribute", - attributes: attrs, - } +type AttributeMap map[string]any +type ClassMap map[string]bool + +func Attributes(attrs *AttributeMap) *AttributeMap { + return attrs } -func Checked() Renderable { +func Checked() Ren { return Attribute("checked", "true") } -func Boost() Renderable { +func Boost() Ren { return Attribute("hx-boost", "true") } -func Attribute(key string, value string) Renderable { - return Attributes(map[string]string{key: value}) +func Attribute(key string, value string) *AttributeMap { + return Attributes(&AttributeMap{key: value}) } -func TriggerChildren() Renderable { +func TriggerChildren() Ren { return HxExtension("trigger-children") } -func HxExtension(value string) Renderable { +func HxExtension(value string) Ren { return Attribute("hx-ext", value) } -func Disabled() Renderable { +func Disabled() Ren { return Attribute("disabled", "") } -func Get(path string) Renderable { +func Get(path string) Ren { return Attribute("hx-get", path) } -func GetPartial(partial func(ctx *RequestContext) *Partial) Renderable { +func GetPartial(partial func(ctx *RequestContext) *Partial) Ren { return Get(GetPartialPath(partial)) } -func GetPartialWithQs(partial func(ctx *RequestContext) *Partial, qs string) Renderable { +func GetPartialWithQs(partial func(ctx *RequestContext) *Partial, qs string) Ren { return Get(GetPartialPathWithQs(partial, qs)) } @@ -130,172 +120,174 @@ func CreateTriggers(triggers ...string) []string { type ReloadParams struct { Triggers []string Target string - Children Renderable + Children Ren } -func ViewOnLoad(partial func(ctx *RequestContext) *Partial) Renderable { +func ViewOnLoad(partial func(ctx *RequestContext) *Partial) Ren { return View(partial, ReloadParams{ Triggers: CreateTriggers("load"), }) } -func View(partial func(ctx *RequestContext) *Partial, params ReloadParams) Renderable { - return Div(Attributes(map[string]string{ +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) Renderable { - return Div(Attributes(map[string]string{ +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) Renderable { +func GetWithQs(path string, qs map[string]string) Ren { return Get(SetQueryParams(path, qs)) } -func Post(url string) Renderable { +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 PostOnClick(url string) Renderable { - return AttributeList(Attribute("hx-post", url), Trigger("click")) +func PostOnTrigger(url string, trigger string) Ren { + return AttributeList(Attribute("hx-post", url), Trigger(trigger)) } -func PostPartialOnClick(partial func(ctx *RequestContext) *Partial) Renderable { +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) Renderable { +func PostPartialOnClickQs(partial func(ctx *RequestContext) *Partial, qs string) Ren { return PostOnClick(GetPartialPathWithQs(partial, qs)) } -func Trigger(trigger string) Renderable { +func Trigger(trigger string) *AttributeMap { return Attribute("hx-trigger", trigger) } -func TextF(format string, args ...interface{}) Renderable { +func TextF(format string, args ...interface{}) Ren { return Text(fmt.Sprintf(format, args...)) } -func Text(text string) Renderable { - return &Node{ - tag: "text", - text: text, - } +func Text(text string) *TextContent { + return NewTextContent(text) } -func Pf(format string, args ...interface{}) Renderable { +func Pf(format string, args ...interface{}) Ren { return P(Text(fmt.Sprintf(format, args...))) } -func Target(target string) Renderable { +func Target(target string) Ren { return Attribute("hx-target", target) } -func Name(name string) Renderable { +func Name(name string) Ren { return Attribute("name", name) } -func Confirm(message string) Renderable { +func Confirm(message string) Ren { return Attribute("hx-confirm", message) } -func Href(path string) Renderable { +func Href(path string) Ren { return Attribute("href", path) } -func Type(name string) Renderable { +func Type(name string) Ren { return Attribute("type", name) } -func Placeholder(placeholder string) Renderable { +func Placeholder(placeholder string) Ren { return Attribute("placeholder", placeholder) } -func OutOfBandSwap(selector string) Renderable { +func OutOfBandSwap(selector string) Ren { return Attribute("hx-swap-oob", Ternary(selector == "", "true", selector)) } -func Click(value string) Renderable { +func Click(value string) Ren { return Attribute("onclick", value) } -func Tag(tag string, children ...Renderable) Renderable { - return &Node{ +func Tag(tag string, children ...Ren) *Element { + return &Element{ tag: tag, children: children, } } -func Html(children ...Renderable) Renderable { +func Html(children ...Ren) *Element { return Tag("html", children...) } -func Head(children ...Renderable) Renderable { +func Head(children ...Ren) *Element { return Tag("head", children...) } -func Body(children ...Renderable) Renderable { +func Body(children ...Ren) *Element { return Tag("body", children...) } -func Link(href string, rel string) Renderable { - return &Node{ - tag: "link", - attributes: map[string]string{ - "href": href, - "rel": rel, - }, - children: make([]Renderable, 0), +func Link(href string, rel string) Ren { + attributeMap := AttributeMap{ + "href": href, + "rel": rel, + } + return &Element{ + tag: "link", + attributes: attributeMap.ToMap(), + children: make([]Ren, 0), } } -func Script(url string) Renderable { - return &Node{ - tag: "script", - attributes: map[string]string{ - "src": url, - "type": "module", - }, - children: make([]Renderable, 0), +func Script(url string) Ren { + attributeMap := AttributeMap{ + "src": url, + } + return &Element{ + tag: "script", + attributes: attributeMap.ToMap(), + children: make([]Ren, 0), } } -func Raw(text string) Renderable { - return &Node{ - tag: "raw", - children: make([]Renderable, 0), - value: text, - } +func Raw(text string) *RawContent { + return NewRawContent(text) } func MultiLineQuotes(text string) string { return "`" + text + "`" } -func RawF(text string, args any) Renderable { - return &Node{ - tag: "raw", - children: make([]Renderable, 0), - value: fmt.Sprintf(text, args), - } +func RawF(text string, args any) *RawContent { + return Raw(fmt.Sprintf(text, args)) } -func RawScript(text string) Renderable { +func RawScript(text string) *RawContent { return Raw("") } -func Pre(children ...Renderable) Renderable { +func Pre(children ...Ren) *Element { return Tag("pre", children...) } -func Div(children ...Renderable) Renderable { +func Div(children ...Ren) *Element { return Tag("div", children...) } @@ -345,24 +337,25 @@ func NewHeaders(headers ...string) *Headers { return &m } -func Checkbox(children ...Renderable) Renderable { +func Checkbox(children ...Ren) Ren { return Input("checkbox", children...) } -func Input(inputType string, children ...Renderable) Renderable { - return &Node{ - tag: "input", - attributes: map[string]string{ - "type": inputType, - }, - children: children, +func Input(inputType string, children ...Ren) Ren { + attributeMap := AttributeMap{ + "type": inputType, + } + return &Element{ + tag: "input", + attributes: attributeMap.ToMap(), + children: children, } } -func List[T any](items []T, mapper func(item T, index int) Renderable) Renderable { - node := &Node{ +func List[T any](items []T, mapper func(item T, index int) *Element) *Element { + node := &Element{ tag: "", - children: make([]Renderable, len(items)), + children: make([]Ren, len(items)), } for index, value := range items { node.children[index] = mapper(value, index) @@ -370,159 +363,138 @@ func List[T any](items []T, mapper func(item T, index int) Renderable) Renderabl return node } -func Fragment(children ...Renderable) Renderable { - return &Node{ +func Fragment(children ...Ren) Ren { + return &Element{ tag: "", children: children, } } -func AttributeList(children ...Renderable) Renderable { - return &Node{ - tag: FlagAttributeList, - children: children, +func AttributeList(children ...*AttributeMap) *AttributeMap { + m := make(AttributeMap) + for _, child := range children { + for k, v := range *child { + m[k] = v + } } + return &m } -func AppendChildren(node *Node, children ...Renderable) Renderable { +func AppendChildren(node *Element, children ...Ren) Ren { node.children = append(node.children, children...) return node } -func Button(children ...Renderable) Renderable { +func Button(children ...Ren) *Element { return Tag("button", children...) } -func Indicator(tag string) Renderable { +func Indicator(tag string) Ren { return Attribute("hx-indicator", tag) } -func P(children ...Renderable) Renderable { +func P(children ...Ren) *Element { return Tag("p", children...) } -func H1(children ...Renderable) Renderable { +func H1(children ...Ren) *Element { return Tag("h1", children...) } -func H2(children ...Renderable) Renderable { +func H2(children ...Ren) *Element { return Tag("h2", children...) } -func H3(children ...Renderable) Renderable { +func H3(children ...Ren) *Element { return Tag("h3", children...) } -func H4(children ...Renderable) Renderable { +func H4(children ...Ren) *Element { return Tag("h4", children...) } -func H5(children ...Renderable) Renderable { +func H5(children ...Ren) *Element { return Tag("h5", children...) } -func H6(children ...Renderable) Renderable { +func H6(children ...Ren) *Element { return Tag("h6", children...) } -func Img(children ...Renderable) Renderable { +func Img(children ...Ren) *Element { return Tag("img", children...) } -func Src(src string) Renderable { +func Src(src string) Ren { return Attribute("src", src) } -func Form(children ...Renderable) Renderable { +func Form(children ...Ren) *Element { return Tag("form", children...) } -func A(children ...Renderable) Renderable { +func A(children ...Ren) *Element { return Tag("a", children...) } -func Nav(children ...Renderable) Renderable { +func Nav(children ...Ren) *Element { return Tag("nav", children...) } -func Empty() Renderable { - return &Node{ +func Empty() *Element { + return &Element{ tag: "", } } -func BeforeRequestSetHtml(children ...Renderable) Renderable { +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) Renderable { +func BeforeRequestSetAttribute(key string, value string) Ren { return Attribute("hx-on::before-request", `this.setAttribute('`+key+`', '`+value+`')`) } -func OnMutationErrorSetText(text string) Renderable { +func OnMutationErrorSetText(text string) Ren { return Attribute("hx-on::mutation-error", `this.innerText = '`+text+`'`) } -func BeforeRequestSetText(text string) Renderable { +func BeforeRequestSetText(text string) Ren { return Attribute("hx-on::before-request", `this.innerText = '`+text+`'`) } -func AfterRequestSetText(text string) Renderable { +func AfterRequestSetText(text string) Ren { return Attribute("hx-on::after-request", `this.innerText = '`+text+`'`) } -func AfterRequestRemoveAttribute(key string, value string) Renderable { +func AfterRequestRemoveAttribute(key string, value string) Ren { return Attribute("hx-on::after-request", `this.removeAttribute('`+key+`')`) } -func IfQueryParam(key string, node *Node) Renderable { +func IfQueryParam(key string, node *Element) Ren { return Fragment(Attribute("hx-if-qp:"+key, "true"), node) } -func Hidden() Renderable { +func Hidden() Ren { return Attribute("style", "display:none") } -func MatchQueryParam(defaultValue string, active string, m map[string]*Node) Renderable { - - rendered := make(map[string]string) - for s, node := range m { - rendered[s] = Render(node) - } - - root := Tag("span", - m[active], - Trigger("url"), - Attribute("hx-match-qp", "true"), - Attribute("hx-match-qp-default", defaultValue), - ) - - for s, node := range rendered { - root = AppendChildren(root.Render(), Attribute("hx-match-qp-mapping:"+s, ``+html.EscapeString(node)+``)) - } - - return root -} - -func AfterRequestSetHtml(children ...Renderable) Renderable { +func AfterRequestSetHtml(children ...Ren) Ren { serialized := Render(Fragment(children...)) return Attribute("hx-on::after-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`) } -func Children(children ...Renderable) Renderable { - return &Node{ - tag: FlagChildrenList, - children: children, - } +func Children(children ...Ren) *ChildList { + return NewChildList(children...) } -func Label(text string) Renderable { +func Label(text string) *Element { return Tag("label", Text(text)) } -func If(condition bool, node Renderable) Renderable { +func If(condition bool, node Ren) Ren { if condition { return node } else { @@ -530,7 +502,7 @@ func If(condition bool, node Renderable) Renderable { } } -func IfElse(condition bool, node Renderable, node2 Renderable) Renderable { +func IfElse(condition bool, node Ren, node2 Ren) Ren { if condition { return node } else { @@ -538,7 +510,7 @@ func IfElse(condition bool, node Renderable, node2 Renderable) Renderable { } } -func IfElseLazy(condition bool, cb1 func() Renderable, cb2 func() Renderable) Renderable { +func IfElseLazy(condition bool, cb1 func() Ren, cb2 func() Ren) Ren { if condition { return cb1() } else { @@ -550,7 +522,7 @@ func GetTriggerName(ctx *RequestContext) string { return ctx.Request().Header.Get("HX-Trigger-Name") } -func IfHtmxRequest(ctx *RequestContext, node Renderable) Renderable { +func IfHtmxRequest(ctx *RequestContext, node Ren) Ren { if ctx.Get("HX-Request") != "" { return node } @@ -559,36 +531,35 @@ func IfHtmxRequest(ctx *RequestContext, node Renderable) Renderable { type SwapArg struct { Selector string - Content *Node + Content *Element } -func NewSwap(selector string, content *Node) SwapArg { +func NewSwap(selector string, content *Element) SwapArg { return SwapArg{ Selector: selector, Content: content, } } -func OobSwap(ctx *RequestContext, content Renderable) Renderable { +func OobSwap(ctx *RequestContext, content *Element) Ren { return OobSwapWithSelector(ctx, "", content) } -func OobSwapWithSelector(ctx *RequestContext, selector string, content Renderable) Renderable { +func OobSwapWithSelector(ctx *RequestContext, selector string, content *Element) Ren { if ctx == nil || ctx.Get("HX-Request") == "" { return Empty() } - c := content.Render() - return c.AppendChild(OutOfBandSwap(selector)) + return content.AppendChild(OutOfBandSwap(selector)) } -func SwapMany(ctx *RequestContext, args ...SwapArg) Renderable { +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) Renderable { + return Fragment(Map(args, func(arg SwapArg) Ren { return arg.Content })...) } @@ -596,12 +567,12 @@ func SwapMany(ctx *RequestContext, args ...SwapArg) Renderable { type OnRequestSwapArgs struct { Target string Get string - Default *Node - BeforeRequest *Node - AfterRequest *Node + Default *Element + BeforeRequest *Element + AfterRequest *Element } -func OnRequestSwap(args OnRequestSwapArgs) Renderable { +func OnRequestSwap(args OnRequestSwapArgs) Ren { return Div(args.Default, BeforeRequestSetHtml(args.BeforeRequest), AfterRequestSetHtml(args.AfterRequest), diff --git a/framework/h/util.go b/framework/h/util.go index 6d8bd21..5d9fd72 100644 --- a/framework/h/util.go +++ b/framework/h/util.go @@ -6,8 +6,8 @@ import ( "net/url" ) -type Renderable interface { - Render() *Node +type Ren interface { + Render() string } func Ternary[T any](value bool, a T, b T) T { diff --git a/sandbox/news/views.go b/sandbox/news/views.go index 67c99e8..05a0935 100644 --- a/sandbox/news/views.go +++ b/sandbox/news/views.go @@ -7,7 +7,7 @@ import ( "time" ) -func StoryList() h.Renderable { +func StoryList() h.Ren { posts, _ := database.GetOrSet[[]Post]("posts", func() []Post { p, _ := List() @@ -21,13 +21,13 @@ func StoryList() h.Renderable { } return h.Fragment( - h.Div(h.List(*posts, func(item Post, index int) h.Renderable { + h.Div(h.List(*posts, func(item Post, index int) h.Ren { return StoryCard(item) })), ) } -func StoryCard(post Post) h.Renderable { +func StoryCard(post Post) h.Ren { url := fmt.Sprintf("/news/%d", post.Id) return h.Div( h.Class("items-center bg-indigo-200 p-4 rounded"), @@ -35,7 +35,7 @@ func StoryCard(post Post) h.Renderable { ) } -func StoryFull(id string) h.Renderable { +func StoryFull(id string) h.Ren { post, err := Get(id) if err != nil { return h.Pf(err.Error()) diff --git a/sandbox/pages/base/root.go b/sandbox/pages/base/root.go index 8000eb9..2b4b9b6 100644 --- a/sandbox/pages/base/root.go +++ b/sandbox/pages/base/root.go @@ -6,7 +6,7 @@ import ( "starter-template/partials/sheet" ) -func RootPage(children ...h.Renderable) h.Renderable { +func RootPage(children ...h.Ren) h.Ren { return h.Html( h.HxExtension("path-deps, response-targets, mutation-error"), h.Head( diff --git a/sandbox/pages/index.go b/sandbox/pages/index.go index 5889746..a9a0f0d 100644 --- a/sandbox/pages/index.go +++ b/sandbox/pages/index.go @@ -65,7 +65,7 @@ func IndexPage(c echo.Context) *h.Page { )) } -func CodeExample() h.Renderable { +func CodeExample() h.Ren { code, err := os.ReadFile("pages/assets/_example.go") scriptSrc, err := os.ReadFile("pages/assets/shiki.js") diff --git a/sandbox/pages/news.index.go b/sandbox/pages/news.index.go index d43b58e..c887ae7 100644 --- a/sandbox/pages/news.index.go +++ b/sandbox/pages/news.index.go @@ -13,7 +13,7 @@ func ListPage(ctx echo.Context) *h.Page { )) } -func list(ctx echo.Context) h.Renderable { +func list(ctx echo.Context) h.Ren { return h.Fragment( h.ViewOnLoad(partials.NewsSheet), h.Div( diff --git a/sandbox/partials/button.go b/sandbox/partials/button.go index badd66b..7b18057 100644 --- a/sandbox/partials/button.go +++ b/sandbox/partials/button.go @@ -5,7 +5,7 @@ import ( "github.com/maddalax/htmgo/framework/h" ) -func OpenSheetButton(open bool, children ...h.Renderable) h.Renderable { +func OpenSheetButton(open bool, children ...h.Ren) h.Ren { if open { return ui.PrimaryButton(ui.ButtonProps{ Id: "open-sheet", diff --git a/sandbox/partials/nav.go b/sandbox/partials/nav.go index cab76c1..68af16c 100644 --- a/sandbox/partials/nav.go +++ b/sandbox/partials/nav.go @@ -7,7 +7,7 @@ type Link struct { Path string } -func NavBar() h.Renderable { +func NavBar() h.Ren { links := []Link{ {"Home", "/"}, @@ -18,7 +18,7 @@ func NavBar() h.Renderable { return h.Nav(h.Class("flex gap-4 items-center p-4 text-slate-600"), h.Boost(), h.Children( - h.Map(links, func(link Link) h.Renderable { + h.Map(links, func(link Link) h.Ren { return h.A(h.Text(link.Name), h.Href(link.Path), h.Class("cursor-pointer hover:text-blue-400")) })..., )) diff --git a/sandbox/partials/news.go b/sandbox/partials/news.go index 4830141..ee588b8 100644 --- a/sandbox/partials/news.go +++ b/sandbox/partials/news.go @@ -36,15 +36,15 @@ func NewsSheetOpenCount(ctx echo.Context) *h.Partial { ) } -func SheetWrapper(children ...h.Renderable) h.Renderable { +func SheetWrapper(children ...h.Ren) h.Ren { return h.Div(h.Id("sheet-partial"), h.Fragment(children...)) } -func SheetClosed() h.Renderable { +func SheetClosed() h.Ren { return h.Div() } -func SheetOpen() h.Renderable { +func SheetOpen() h.Ren { return h.Fragment(h.Div( h.Class(`fixed top-0 right-0 h-full w-96 bg-gray-100 shadow-lg z-50`), h.Div( diff --git a/sandbox/partials/patient/patient.go b/sandbox/partials/patient/patient.go index 5986c66..fa9ef10 100644 --- a/sandbox/partials/patient/patient.go +++ b/sandbox/partials/patient/patient.go @@ -43,7 +43,7 @@ func AddPatientSheetPartial(ctx echo.Context) *h.Partial { ) } -func AddPatientSheet(onClosePath string) h.Renderable { +func AddPatientSheet(onClosePath string) h.Ren { return sheet.Opened( sheet.Props{ OnClosePath: onClosePath, @@ -81,7 +81,7 @@ func ValidateForm(ctx echo.Context) *h.Partial { return h.NewPartial(h.Fragment()) } -func addPatientForm() h.Renderable { +func addPatientForm() h.Ren { return h.Form( h.HxExtension("debug, trigger-children"), h.Attribute("hx-target-5*", "#submit-error"), @@ -123,7 +123,7 @@ func addPatientForm() h.Renderable { ) } -func Row(patient *patient.Patient, index int) h.Renderable { +func Row(patient *patient.Patient, index int) h.Ren { return h.Div( h.Class("flex flex-col gap-2 rounded p-4", h.Ternary(index%2 == 0, "bg-red-100", "")), h.Pf("Name: %s", patient.Name), @@ -131,7 +131,7 @@ func Row(patient *patient.Patient, index int) h.Renderable { ) } -func AddPatientButton() h.Renderable { +func AddPatientButton() h.Ren { return ui.Button(ui.ButtonProps{ Id: "add-patient", Text: "Add Patient", diff --git a/sandbox/partials/sheet/sheet.go b/sandbox/partials/sheet/sheet.go index 56aafcb..bb5a446 100644 --- a/sandbox/partials/sheet/sheet.go +++ b/sandbox/partials/sheet/sheet.go @@ -8,13 +8,13 @@ import ( type Props struct { ClassName string - Root h.Renderable + Root h.Ren OnClosePath string } var Id = "#active-modal" -func Opened(props Props) h.Renderable { +func Opened(props Props) h.Ren { return h.Fragment(h.Div( h.Class(`fixed top-0 right-0 h-full shadow-lg z-50`, h.Ternary(props.ClassName != "", props.ClassName, "w-96 bg-gray-100")), @@ -24,7 +24,7 @@ func Opened(props Props) h.Renderable { ))) } -func Closed() h.Renderable { +func Closed() h.Ren { return h.Div(h.Id(Id)) } @@ -35,7 +35,7 @@ func Close(ctx echo.Context) *h.Partial { ) } -func closeButton(props Props) h.Renderable { +func closeButton(props Props) h.Ren { return h.Div( h.Class("absolute top-0 right-0 p-3"), h.Button( diff --git a/starter-template/pages/base/root.go b/starter-template/pages/base/root.go index 9a98ddb..4e4981c 100644 --- a/starter-template/pages/base/root.go +++ b/starter-template/pages/base/root.go @@ -13,7 +13,7 @@ func Extensions() string { return strings.Join(extensions, ", ") } -func RootPage(children ...h.Renderable) h.Renderable { +func RootPage(children ...h.Ren) h.Ren { return h.Html( h.HxExtension(Extensions()), h.Head( diff --git a/starter-template/pages/index.go b/starter-template/pages/index.go index 1f16b47..7f75508 100644 --- a/starter-template/pages/index.go +++ b/starter-template/pages/index.go @@ -27,7 +27,7 @@ func IndexPage(c echo.Context) *h.Page { )) } -func Button() h.Renderable { +func Button() h.Ren { return h.Button(h.Class("btn bg-green-500 p-4 rounded text-white"), h.Text("my button"), h.AfterRequest( diff --git a/todo-list/pages/base/root.go b/todo-list/pages/base/root.go index fbd77dc..6500995 100644 --- a/todo-list/pages/base/root.go +++ b/todo-list/pages/base/root.go @@ -13,7 +13,7 @@ func Extensions() string { return strings.Join(extensions, ", ") } -func RootPage(children ...h.Renderable) h.Renderable { +func RootPage(children ...h.Ren) h.Ren { return h.Html( h.HxExtension(Extensions()), h.Head( diff --git a/todo-list/pages/index.go b/todo-list/pages/index.go index edc73dd..7795f22 100644 --- a/todo-list/pages/index.go +++ b/todo-list/pages/index.go @@ -27,7 +27,7 @@ func IndexPage(c echo.Context) *h.Page { )) } -func Button() h.Renderable { +func Button() h.Ren { return h.Button(h.Class("btn bg-green-500 p-4 rounded text-white"), h.Text("my button"), h.AfterRequest( diff --git a/todo-list/pages/tasks.go b/todo-list/pages/tasks.go index a747cef..65b5cac 100644 --- a/todo-list/pages/tasks.go +++ b/todo-list/pages/tasks.go @@ -20,6 +20,9 @@ func TaskListPage(ctx *h.RequestContext) *h.Page { h.Class("flex flex-col gap-6 p-4 items-center max-w-xl mx-auto pb-12"), title, task.Card(ctx), + h.Children( + h.Div(h.Text("Double-click to edit a todo")), + ), ), ), )) diff --git a/todo-list/partials/task/task.go b/todo-list/partials/task/task.go index 1940453..87e74d1 100644 --- a/todo-list/partials/task/task.go +++ b/todo-list/partials/task/task.go @@ -23,7 +23,7 @@ func getActiveTab(ctx *h.RequestContext) Tab { return TabAll } -func Card(ctx *h.RequestContext) h.Renderable { +func Card(ctx *h.RequestContext) *h.Element { service := tasks.NewService(ctx.ServiceLocator()) list, _ := service.List() @@ -34,7 +34,7 @@ func Card(ctx *h.RequestContext) h.Renderable { ) } -func CardBody(list []*ent.Task, tab Tab) h.Renderable { +func CardBody(list []*ent.Task, tab Tab) *h.Element { return h.Div( h.Id("tasks-card-body"), Input(list), @@ -43,7 +43,7 @@ func CardBody(list []*ent.Task, tab Tab) h.Renderable { ) } -func Input(list []*ent.Task) h.Renderable { +func Input(list []*ent.Task) *h.Element { return h.Div( h.Id("task-card-input"), h.Class("border border-b-slate-100 relative"), @@ -61,7 +61,7 @@ func Input(list []*ent.Task) h.Renderable { ) } -func CompleteAllIcon(list []*ent.Task) h.Renderable { +func CompleteAllIcon(list []*ent.Task) *h.Element { notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool { return item.CompletedAt == nil })) @@ -74,7 +74,7 @@ func CompleteAllIcon(list []*ent.Task) h.Renderable { ) } -func Footer(list []*ent.Task, activeTab Tab) h.Renderable { +func Footer(list []*ent.Task, activeTab Tab) *h.Element { notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool { return item.CompletedAt == nil @@ -90,7 +90,7 @@ func Footer(list []*ent.Task, activeTab Tab) h.Renderable { ), h.Div( h.Class("flex items-center gap-4"), - h.List(tabs, func(tab Tab, index int) h.Renderable { + h.List(tabs, func(tab Tab, index int) *h.Element { return h.P( h.PostOnClick(h.GetPartialPathWithQs(ChangeTab, "tab="+tab)), h.ClassX("cursor-pointer px-2 py-1 rounded", map[string]bool{ @@ -110,12 +110,12 @@ func Footer(list []*ent.Task, activeTab Tab) h.Renderable { ) } -func List(list []*ent.Task, tab Tab) h.Renderable { +func List(list []*ent.Task, tab Tab) *h.Element { return h.Div( h.Id("task-card-list"), h.Class("bg-white w-full"), h.Div( - h.List(list, func(item *ent.Task, index int) h.Renderable { + h.List(list, func(item *ent.Task, index int) *h.Element { if tab == TabActive && item.CompletedAt != nil { return h.Empty() } @@ -128,10 +128,10 @@ func List(list []*ent.Task, tab Tab) h.Renderable { ) } -func Task(task *ent.Task, editing bool) h.Renderable { +func Task(task *ent.Task, editing bool) *h.Element { return h.Div( h.Id(fmt.Sprintf("task-%s", task.ID.String())), - h.ClassX("h-[80px] max-h-[80px] max-w-2xl flex items-center p-4 gap-4 cursor-pointer", map[string]bool{ + h.ClassX("h-[80px] max-h-[80px] max-w-2xl flex items-center p-4 gap-4 cursor-pointer", h.ClassMap{ "border border-b-slate-100": !editing, }), CompleteIcon(task), @@ -147,13 +147,17 @@ func Task(task *ent.Task, editing bool) h.Renderable { ), h.Input( "text", - h.Post(h.GetPartialPath(UpdateName)), - h.Trigger("blur, keyup[keyCode==13]"), - h.Attribute("autocomplete", "off"), - h.Attribute("autofocus", "true"), - h.Attribute("name", "name"), - h.Class("pl-1 h-full w-full text-xl outline-none outline-2 outline-rose-300"), - h.Attribute("value", task.Name), + h.PostPartialOnTrigger(UpdateName, h.TriggerBlur, h.TriggerKeyUpEnter), + h.Attributes(&h.AttributeMap{ + "placeholder": "What needs to be done?", + "autofocus": "true", + "autocomplete": "off", + "name": "name", + "class": h.ClassX("", h.ClassMap{ + "pl-1 h-full w-full text-xl outline-none outline-2 outline-rose-300": true, + }), + "value": task.Name, + }), ), ), ), @@ -168,7 +172,7 @@ func Task(task *ent.Task, editing bool) h.Renderable { ) } -func CompleteIcon(task *ent.Task) h.Renderable { +func CompleteIcon(task *ent.Task) *h.Element { return h.Div( h.Trigger("click"), h.Post(h.GetPartialPathWithQs(ToggleCompleted, "id="+task.ID.String())),