more cleanup

This commit is contained in:
maddalax 2024-09-21 11:52:56 -05:00
parent c555da4dc9
commit da7e22c446
16 changed files with 444 additions and 344 deletions

View file

@ -47,10 +47,10 @@ func Button(props ButtonProps) h.Ren {
button := h.Button(
h.If(props.Id != "", h.Id(props.Id)),
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.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")),
lifecycle,
text,

View file

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/maddalax/htmgo/framework/htmgo/service"
"github.com/maddalax/htmgo/framework/hx"
"github.com/maddalax/htmgo/framework/util/process"
"log/slog"
"time"
@ -11,7 +12,14 @@ import (
type RequestContext struct {
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 {
@ -47,6 +55,16 @@ func Start(opts AppOpts) {
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() {
if a.Opts.Register != nil {
@ -56,9 +74,10 @@ func (a App) start() {
a.Echo.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cc := &RequestContext{
c,
a.Opts.ServiceLocator,
Context: c,
locator: a.Opts.ServiceLocator,
}
populateHxFields(cc)
return next(cc)
}
})

View file

@ -1,6 +1,10 @@
package h
import "fmt"
import (
"fmt"
"github.com/maddalax/htmgo/framework/hx"
"strings"
)
type AttributeMap map[string]any
@ -21,3 +25,143 @@ func (m *AttributeMap) ToMap() map[string]string {
}
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)
}

View file

@ -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 {
return runtime.FuncForPC(reflect.ValueOf(partial).Pointer()).Name()
}

View file

@ -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 {
if condition {
return node
@ -30,3 +37,10 @@ func IfHtmxRequest(ctx *RequestContext, node Ren) Ren {
}
return Empty()
}
func ClassIf(condition bool, value string) Ren {
if condition {
return Class(value)
}
return Empty()
}

41
framework/h/header.go Normal file
View 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
View 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()
}

View file

@ -6,6 +6,10 @@ import (
"time"
)
type Ren interface {
Render(builder *strings.Builder)
}
func Render(node Ren) string {
start := time.Now()
builder := &strings.Builder{}

83
framework/h/swap.go Normal file
View 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
})...)
}

View file

@ -1,50 +1,10 @@
package h
import (
"encoding/json"
"fmt"
"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) 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 ClassMap map[string]bool
type PartialFunc = func(ctx *RequestContext) *Partial
@ -59,113 +19,7 @@ func (node *Element) AppendChild(child Ren) *Element {
return node
}
func Data(data map[string]any) Ren {
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 {
func TextF(format string, args ...interface{}) *TextContent {
return Text(fmt.Sprintf(format, args...))
}
@ -177,39 +31,6 @@ func Pf(format string, args ...interface{}) Ren {
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 {
return &Element{
tag: tag,
@ -292,52 +113,6 @@ func Article(children ...Ren) *Element {
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 {
return Input("checkbox", children...)
}
@ -368,14 +143,8 @@ func Fragment(children ...Ren) *ChildList {
return 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 Template(children ...Ren) *Element {
return Tag("template", children...)
}
func AppendChildren(node *Element, children ...Ren) Ren {
@ -388,10 +157,6 @@ func Button(children ...Ren) *Element {
return Tag("button", children...)
}
func Indicator(tag string) *AttributeMap {
return Attribute(hx.IndicatorAttr, tag)
}
func P(children ...Ren) *Element {
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 {
return NewChildList(children...)
}
@ -461,42 +218,3 @@ func Children(children ...Ren) *ChildList {
func Label(text string) *Element {
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
})...)
}

View file

@ -2,22 +2,8 @@ package h
import (
"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 {
serialized, err := json.Marshal(data)
if err != nil {
@ -25,34 +11,3 @@ func JsonSerialize(data any) string {
}
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()
}

View file

@ -14,7 +14,7 @@ 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 {
func GetWithQs(path string, qs *Qs, trigger string) *AttributeMap {
return Get(SetQueryParams(path, qs), trigger)
}

View file

@ -3,6 +3,7 @@ package hx
type Attribute = string
type Header = string
type Event = string
type SwapType = string
// https://htmx.org/reference/#events
const (
@ -42,6 +43,12 @@ 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"
PushUrlHeader Header = "HX-Push-Url"
RedirectHeader Header = "HX-Redirect"
@ -136,3 +143,16 @@ const (
DropEvent Event = "ondrop"
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"
)

View file

@ -10,8 +10,9 @@ type NavItem struct {
}
func ToggleNavbar(ctx *h.RequestContext) *h.Partial {
return h.NewPartial(
h.OobSwap(ctx, MobileNav(h.GetQueryParam(ctx, "expanded") == "true")),
return h.SwapManyPartial(
ctx,
MobileNav(h.GetQueryParam(ctx, "expanded") == "true"),
)
}

View file

@ -29,7 +29,7 @@ func Create(ctx echo.Context) *h.Partial {
}
headers := h.CombineHeaders(h.PushQsHeader(ctx, "adding", ""), &map[string]string{
"HX-Trigger": "patient-added",
"HX-HxTrigger": "patient-added",
})
return h.NewPartialWithHeaders(

View file

@ -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.Placeholder("What needs to be done?"),
h.Post(h.GetPartialPath(Create)),
h.Trigger("keyup[keyCode==13]"),
h.HxTrigger("keyup[keyCode==13]"),
),
CompleteAllIcon(list),
)
@ -162,7 +162,7 @@ func Task(task *ent.Task, editing bool) *h.Element {
),
),
h.P(
h.Trigger("dblclick"),
h.HxTrigger("dblclick"),
h.GetPartialWithQs(EditNameForm, "id="+task.ID.String()),
h.ClassX("text-xl break-all text-wrap truncate", map[string]bool{
"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 {
return h.Div(
h.Trigger("click"),
h.HxTrigger("click"),
h.Post(h.GetPartialPathWithQs(ToggleCompleted, "id="+task.ID.String())),
h.Class("flex items-center justify-center cursor-pointer"),
h.Div(