diff --git a/docs/architecture.md b/docs/architecture.md index 3398163..23800b8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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`, and `Entitled` wrappers. Returns the node for chaining. | +| `Attr(Node, key, value)` | Sets an attribute on an `El` node. Traverses through `If`, `Unless`, `Entitled`, `Switch`, and iterator wrappers. Returns the node for chaining. | | `Text(key, ...any)` | Translated text via `go-i18n`. Output is always HTML-escaped. | | `Raw(content)` | Unescaped trusted content. Explicit escape hatch. | | `If(cond, Node)` | Renders the child only when the condition function returns true. | diff --git a/node.go b/node.go index 837e2d8..ac44af5 100644 --- a/node.go +++ b/node.go @@ -149,7 +149,8 @@ func El(tag string, children ...Node) Node { } // Attr sets an attribute on an El node. Returns the node for chaining. -// It recursively traverses through wrappers like If, Unless, and Entitled. +// It recursively traverses through wrappers like If, Unless, Entitled, +// Switch, and Each/EachSeq so the attribute lands on the rendered element. func Attr(n Node, key, value string) Node { switch t := n.(type) { case *elNode: @@ -160,6 +161,12 @@ func Attr(n Node, key, value string) Node { Attr(t.node, key, value) case *entitledNode: Attr(t.node, key, value) + case *switchNode: + for caseKey, caseNode := range t.cases { + t.cases[caseKey] = Attr(caseNode, key, value) + } + case interface{ setAttr(string, string) }: + t.setAttr(key, value) } return n } @@ -415,3 +422,14 @@ func (n *eachNode[T]) renderWithPath(ctx *Context, path string) string { } return b.String() } + +func (n *eachNode[T]) setAttr(key, value string) { + if n == nil || n.fn == nil { + return + } + + prev := n.fn + n.fn = func(item T) Node { + return Attr(prev(item), key, value) + } +} diff --git a/node_test.go b/node_test.go index 1eac36a..3adc770 100644 --- a/node_test.go +++ b/node_test.go @@ -5,6 +5,7 @@ import ( "testing" i18n "dappco.re/go/core/i18n" + "slices" ) func TestRawNode_Render(t *testing.T) { @@ -378,6 +379,52 @@ func TestAttr_ThroughEntitledNode(t *testing.T) { } } +func TestAttr_ThroughSwitchNode(t *testing.T) { + ctx := NewContext() + node := Switch(func(*Context) string { return "dark" }, map[string]Node{ + "dark": El("div", Raw("content")), + "light": El("div", Raw("other")), + }) + + Attr(node, "class", "theme") + + got := node.Render(ctx) + want := `
content
` + if got != want { + t.Errorf("Attr through Switch = %q, want %q", got, want) + } +} + +func TestAttr_ThroughEachNode(t *testing.T) { + ctx := NewContext() + node := Each([]string{"a", "b"}, func(item string) Node { + return El("span", Raw(item)) + }) + + Attr(node, "class", "item") + + got := node.Render(ctx) + want := `ab` + if got != want { + t.Errorf("Attr through Each = %q, want %q", got, want) + } +} + +func TestAttr_ThroughEachSeqNode(t *testing.T) { + ctx := NewContext() + node := EachSeq(slices.Values([]string{"a", "b"}), func(item string) Node { + return El("span", Raw(item)) + }) + + Attr(node, "class", "item") + + got := node.Render(ctx) + want := `ab` + if got != want { + t.Errorf("Attr through EachSeq = %q, want %q", got, want) + } +} + func TestTextNode_WithService(t *testing.T) { svc, _ := i18n.New() ctx := NewContextWithService(svc)