Repository: maragudk/gomponents (go)

Issue:
Add varargs to Group?
In addition to the slice of Nodes.

Pull request description:
Make `Group` a type
This changes `Group` to be a type instead of a function, which means it can support both grouping `[]Node` as well as doing variadic-ish things like `Group{n1, n2}`.

This also means that `Map` can just return a `Group`.

Special thanks to @deitrix for this simple and elegant solution that, for some reason or another, has had me perplexed for a long time.

Fixes #201.

Patch:
diff --git a/gomponents.go b/gomponents.go
index 1a3b4ee..a17e1da 100644
--- a/gomponents.go
+++ b/gomponents.go
@@ -113,8 +113,8 @@ func renderChild(w *statefulWriter, c Node, t NodeType) {
 
 	// 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 {
+	if g, ok := c.(Group); ok {
+		for _, groupC := range g {
 			renderChild(w, groupC, t)
 		}
 		return
@@ -246,8 +246,8 @@ func Rawf(format string, a ...interface{}) Node {
 	})
 }
 
-// Map a slice of anything to a slice of Nodes.
-func Map[T any](ts []T, cb func(T) Node) []Node {
+// Map a slice of anything to a [Group] (which is just a slice of [Node]s).
+func Map[T any](ts []T, cb func(T) Node) Group {
 	var nodes []Node
 	for _, t := range ts {
 		nodes = append(nodes, cb(t))
@@ -255,27 +255,21 @@ func Map[T any](ts []T, cb func(T) Node) []Node {
 	return nodes
 }
 
-type group struct {
-	children []Node
-}
+// Group a slice of [Node]s into one Node, while still being usable like a regular slice of [Node]s.
+// A [Group] can render directly, but if any of the direct children are [AttributeType], they will be ignored,
+// to not produce invalid HTML.
+type Group []Node
 
 // String satisfies [fmt.Stringer].
-func (g group) String() string {
+func (g Group) String() string {
 	var b strings.Builder
 	_ = g.Render(&b)
 	return b.String()
 }
 
 // Render satisfies [Node].
-func (g group) Render(w io.Writer) error {
-	return render(w, nil, g.children...)
-}
-
-// 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}
+func (g Group) Render(w io.Writer) error {
+	return render(w, nil, g...)
 }
 
 // If condition is true, return the given [Node]. Otherwise, return nil.


Test patch:
diff --git a/gomponents_test.go b/gomponents_test.go
index 27fb534..c8dab1b 100644
--- a/gomponents_test.go
+++ b/gomponents_test.go
@@ -228,7 +228,7 @@ func ExampleRawf() {
 }
 
 func TestMap(t *testing.T) {
-	t.Run("maps slices to nodes", func(t *testing.T) {
+	t.Run("maps a slice to a group", func(t *testing.T) {
 		items := []string{"hat", "partyhat", "turtlehat"}
 		lis := g.Map(items, func(i string) g.Node {
 			return g.El("li", g.Text(i))
@@ -237,14 +237,20 @@ func TestMap(t *testing.T) {
 		list := g.El("ul", lis...)
 
 		assert.Equal(t, `<ul><li>hat</li><li>partyhat</li><li>turtlehat</li></ul>`, list)
+		if len(lis) != 3 {
+			t.FailNow()
+		}
+		assert.Equal(t, `<li>hat</li>`, lis[0])
+		assert.Equal(t, `<li>partyhat</li>`, lis[1])
+		assert.Equal(t, `<li>turtlehat</li>`, lis[2])
 	})
 }
 
 func ExampleMap() {
 	items := []string{"party hat", "super hat"}
-	e := g.El("ul", g.Group(g.Map(items, func(i string) g.Node {
+	e := g.El("ul", g.Map(items, func(i string) g.Node {
 		return g.El("li", g.Text(i))
-	})))
+	}))
 	_ = e.Render(os.Stdout)
 	// Output: <ul><li>party hat</li><li>super hat</li></ul>
 }
@@ -262,25 +268,26 @@ func TestGroup(t *testing.T) {
 		assert.Equal(t, "<div></div><span></span>", e)
 	})
 
-	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")}
+	t.Run("does not ignore attributes at the second level and below", func(t *testing.T) {
+		children := []g.Node{g.El("div", g.Attr("class", "hat"), g.El("hr", g.Attr("id", "partyhat"))), 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)
+		assert.Equal(t, `<div class="hat"><hr id="partyhat"></div><span></span>`, 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>" {
+		if e, ok := any(e).(fmt.Stringer); !ok || e.String() != "<div></div><span></span>" {
 			t.FailNow()
 		}
 	})
+
+	t.Run("can be used like a regular slice", func(t *testing.T) {
+		e := g.Group{g.El("div"), g.El("span")}
+		assert.Equal(t, "<div></div><span></span>", e)
+		assert.Equal(t, "<div></div>", e[0])
+		assert.Equal(t, "<span></span>", e[1])
+	})
 }
 
 func ExampleGroup() {
@@ -290,6 +297,12 @@ func ExampleGroup() {
 	// Output: <div></div><span></span>
 }
 
+func ExampleGroup_slice() {
+	e := g.Group{g.El("div"), g.El("span")}
+	_ = 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")))