feat(html): recurse attr through layout trees
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-15 02:30:05 +01:00
parent dc5edcfb3d
commit 1c2b2c532b
3 changed files with 40 additions and 2 deletions

View file

@ -22,7 +22,7 @@ All concrete node types are unexported structs with exported constructor functio
| Constructor | Behaviour |
|-------------|-----------|
| `El(tag, ...Node)` | HTML element with children. Void elements (`br`, `img`, `input`, etc.) never emit a closing tag. |
| `Attr(Node, key, value)` | Sets an attribute on an `El` node. Traverses through `If`, `Unless`, `Entitled`, `Each`, `EachSeq`, and `Switch` wrappers. Returns the node for chaining. |
| `Attr(Node, key, value)` | Sets an attribute on an `El` node. Traverses through `If`, `Unless`, `Entitled`, `Each`, `EachSeq`, `Switch`, `Layout`, and `Responsive` wrappers. Returns the node for chaining. |
| `AriaLabel(Node, label)` | Convenience helper that sets `aria-label` on an element node. |
| `AltText(Node, text)` | Convenience helper that sets `alt` on an element node. |
| `TabIndex(Node, index)` | Convenience helper that sets `tabindex` on an element node. |

16
node.go
View file

@ -95,7 +95,8 @@ func El(tag string, children ...Node) Node {
// Attr sets an attribute on an El node. Returns the node for chaining.
// Usage example: Attr(El("a", Text("docs")), "href", "/docs")
// It recursively traverses through wrappers like If, Unless, Entitled, and Each.
// It recursively traverses through wrappers like If, Unless, Entitled, Each,
// Layout, and Responsive when present.
func Attr(n Node, key, value string) Node {
if n == nil {
return n
@ -114,6 +115,19 @@ func Attr(n Node, key, value string) Node {
for _, child := range t.cases {
Attr(child, key, value)
}
case *Layout:
if t.slots != nil {
for slot, children := range t.slots {
for i := range children {
children[i] = Attr(children[i], key, value)
}
t.slots[slot] = children
}
}
case *Responsive:
for i := range t.variants {
Attr(t.variants[i].layout, key, value)
}
case attrApplier:
t.applyAttr(key, value)
}

View file

@ -481,6 +481,30 @@ func TestAttr_ThroughSwitchNode_Good(t *testing.T) {
}
}
func TestAttr_ThroughLayout_Good(t *testing.T) {
ctx := NewContext()
layout := NewLayout("C").C(El("div", Raw("content")))
Attr(layout, "class", "page")
got := layout.Render(ctx)
want := `<main role="main" data-block="C"><div class="page" data-block="C.0">content</div></main>`
if got != want {
t.Errorf("Attr through Layout = %q, want %q", got, want)
}
}
func TestAttr_ThroughResponsive_Good(t *testing.T) {
ctx := NewContext()
resp := NewResponsive().Variant("mobile", NewLayout("C").C(El("div", Raw("content"))))
Attr(resp, "data-kind", "page")
got := resp.Render(ctx)
want := `<div data-variant="mobile"><main role="main" data-block="C"><div data-block="C.0" data-kind="page">content</div></main></div>`
if got != want {
t.Errorf("Attr through Responsive = %q, want %q", got, want)
}
}
func TestAttr_ThroughEachNode_Good(t *testing.T) {
ctx := NewContext()
node := Each([]string{"a", "b"}, func(item string) Node {