diff options
-rw-r--r-- | gomponents.go | 45 | ||||
-rw-r--r-- | gomponents_test.go | 49 |
2 files changed, 56 insertions, 38 deletions
diff --git a/gomponents.go b/gomponents.go index 952e32c..ffe8483 100644 --- a/gomponents.go +++ b/gomponents.go @@ -72,10 +72,16 @@ func (n NodeFunc) String() string { // If an element is a void element, non-attribute children nodes are ignored. // Use this if no convenience creator exists in the html package. func El(name string, children ...Node) Node { - return NodeFunc(func(w2 io.Writer) error { - w := &statefulWriter{w: w2} + return NodeFunc(func(w io.Writer) error { + return render(w, &name, children...) + }) +} + +func render(w2 io.Writer, name *string, children ...Node) error { + w := &statefulWriter{w: w2} - w.Write([]byte("<" + name)) + if name != nil { + w.Write([]byte("<" + *name)) for _, c := range children { renderChild(w, c, AttributeType) @@ -83,17 +89,20 @@ func El(name string, children ...Node) Node { w.Write([]byte(">")) - if isVoidElement(name) { + if isVoidElement(*name) { return w.err } + } - for _, c := range children { - renderChild(w, c, ElementType) - } + for _, c := range children { + renderChild(w, c, ElementType) + } - w.Write([]byte("</" + name + ">")) - return w.err - }) + if name != nil { + w.Write([]byte("</" + *name + ">")) + } + + return w.err } // renderChild c to the given writer w if the node type is t. @@ -102,6 +111,8 @@ func renderChild(w *statefulWriter, c Node, t NodeType) { return } + // Rendering groups like this is still important even though a group can render itself, + // since otherwise attributes will sometimes be ignored. if g, ok := c.(group); ok { for _, groupC := range g.children { renderChild(w, groupC, t) @@ -241,17 +252,19 @@ type group struct { // String satisfies [fmt.Stringer]. func (g group) String() string { - panic("cannot render group directly") + var b strings.Builder + _ = g.Render(&b) + return b.String() } // Render satisfies [Node]. -func (g group) Render(io.Writer) error { - panic("cannot render group directly") +func (g group) Render(w io.Writer) error { + return render(w, nil, g.children...) } -// Group multiple Nodes into one Node. Useful for concatenation of Nodes in variadic functions. -// The resulting Node cannot Render directly, trying it will panic. -// Render must happen through a parent element created with El or a helper. +// Group a slice of Nodes into one Node. Useful for grouping the result of [Map] into one [Node]. +// A [Group] can render directly, but if any of the direct children are [AttributeType], they will be ignored, +// to not produce invalid HTML. func Group(children []Node) Node { return group{children: children} } diff --git a/gomponents_test.go b/gomponents_test.go index c0ae7a7..f3e81b0 100644 --- a/gomponents_test.go +++ b/gomponents_test.go @@ -234,35 +234,40 @@ func TestGroup(t *testing.T) { assert.Equal(t, `<div class="foo"><img><br id="hat"><hr></div>`, e) }) - t.Run("panics on direct render", func(t *testing.T) { - e := g.Group(nil) - panicked := false - defer func() { - if err := recover(); err != nil { - panicked = true - } - }() - _ = e.Render(nil) - if !panicked { - t.FailNow() - } + t.Run("ignores attributes at the first level", func(t *testing.T) { + children := []g.Node{g.Attr("class", "hat"), g.El("div"), g.El("span")} + e := g.Group(children) + assert.Equal(t, "<div></div><span></span>", e) }) - t.Run("panics on direct string", func(t *testing.T) { - e := g.Group(nil).(fmt.Stringer) - panicked := false - defer func() { - if err := recover(); err != nil { - panicked = true - } - }() - _ = e.String() - if !panicked { + t.Run("does not ignore attributes at the second level", func(t *testing.T) { + children := []g.Node{g.El("div", g.Attr("class", "hat")), g.El("span")} + e := g.Group(children) + assert.Equal(t, `<div class="hat"></div><span></span>`, e) + }) + + t.Run("can render a group child node including attributes", func(t *testing.T) { + children := []g.Node{g.Attr("id", "hat"), g.El("div"), g.El("span")} + e := g.El("div", g.Group(children)) + assert.Equal(t, `<div id="hat"><div></div><span></span></div>`, e) + }) + + t.Run("implements fmt.Stringer", func(t *testing.T) { + children := []g.Node{g.El("div"), g.El("span")} + e := g.Group(children) + if e, ok := e.(fmt.Stringer); !ok || e.String() != "<div></div><span></span>" { t.FailNow() } }) } +func ExampleGroup() { + children := []g.Node{g.El("div"), g.El("span")} + e := g.Group(children) + _ = e.Render(os.Stdout) + // Output: <div></div><span></span> +} + func TestIf(t *testing.T) { t.Run("returns node if condition is true", func(t *testing.T) { n := g.El("div", g.If(true, g.El("span"))) |