Repository: maragudk/gomponents
Pull request #29 (2020-10-22 07:05:37)

Add Group function to group Nodes

codecov[bot]: # [Codecov](https://codecov.io/gh/maragudk/gomponents/pull/29?src=pr&el=h1) Report
> Merging [#29](https://codecov.io/gh/maragudk/gomponents/pull/29?src=pr&el=desc) into [master](https://codecov.io/gh/maragudk/gomponents/commit/f2a2b949704e2faa7117dd33aae8da551a4baf8e?el=desc) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/maragudk/gomponents/pull/29/graphs/tree.svg?width=650&height=150&src=pr&token=JYN032TYAC)](https://codecov.io/gh/maragudk/gomponents/pull/29?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master       #29   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            9         9           
  Lines          116       116           
=========================================
  Hits           116       116           
```


| [Impacted Files](https://codecov.io/gh/maragudk/gomponents/pull/29?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [el/document.go](https://codecov.io/gh/maragudk/gomponents/pull/29/diff?src=pr&el=tree#diff-ZWwvZG9jdW1lbnQuZ28=) | `100.00% <ø> (ø)` | |
| [el/form.go](https://codecov.io/gh/maragudk/gomponents/pull/29/diff?src=pr&el=tree#diff-ZWwvZm9ybS5nbw==) | `100.00% <100.00%> (ø)` | |
| [el/inline.go](https://codecov.io/gh/maragudk/gomponents/pull/29/diff?src=pr&el=tree#diff-ZWwvaW5saW5lLmdv) | `100.00% <100.00%> (ø)` | |
| [el/media.go](https://codecov.io/gh/maragudk/gomponents/pull/29/diff?src=pr&el=tree#diff-ZWwvbWVkaWEuZ28=) | `100.00% <100.00%> (ø)` | |
| [el/section.go](https://codecov.io/gh/maragudk/gomponents/pull/29/diff?src=pr&el=tree#diff-ZWwvc2VjdGlvbi5nbw==) | `100.00% <100.00%> (ø)` | |
| [gomponents.go](https://codecov.io/gh/maragudk/gomponents/pull/29/diff?src=pr&el=tree#diff-Z29tcG9uZW50cy5nbw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/maragudk/gomponents/pull/29?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/maragudk/gomponents/pull/29?src=pr&el=footer). Last update [f2a2b94...a46903a](https://codecov.io/gh/maragudk/gomponents/pull/29?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/el/document.go b/el/document.go
index 8520cd7..370a124 100644
--- a/el/document.go
+++ b/el/document.go
@@ -55,15 +55,3 @@ func Style(children ...g.Node) g.NodeFunc {
 func Base(children ...g.Node) g.NodeFunc {
 	return g.El("base", children...)
 }
-
-func prepend(node g.Node, nodes []g.Node) []g.Node {
-	newNodes := []g.Node{node}
-	newNodes = append(newNodes, nodes...)
-	return newNodes
-}
-
-func prepend2(node1, node2 g.Node, nodes []g.Node) []g.Node {
-	newNodes := []g.Node{node1, node2}
-	newNodes = append(newNodes, nodes...)
-	return newNodes
-}
diff --git a/el/form.go b/el/form.go
index 9ada591..25f89c7 100644
--- a/el/form.go
+++ b/el/form.go
@@ -13,40 +13,40 @@ func Button(children ...g.Node) g.NodeFunc {
 
 // Form returns an element with name "form", the given action and method attributes, and the given children.
 func Form(action, method string, children ...g.Node) g.NodeFunc {
-	return g.El("form", prepend2(g.Attr("action", action), g.Attr("method", method), children)...)
+	return g.El("form", g.Attr("action", action), g.Attr("method", method), g.Group(children))
 }
 
 // Input returns an element with name "input", the given type and name attributes, and the given children.
 // Note that "type" is a keyword in Go, so the parameter is called typ.
 func Input(typ, name string, children ...g.Node) g.NodeFunc {
-	return g.El("input", prepend2(g.Attr("type", typ), g.Attr("name", name), children)...)
+	return g.El("input", g.Attr("type", typ), g.Attr("name", name), g.Group(children))
 }
 
 // Label returns an element with name "label", the given for attribute, and the given children.
 // Note that "for" is a keyword in Go, so the parameter is called forr.
 func Label(forr string, children ...g.Node) g.NodeFunc {
-	return g.El("label", prepend(g.Attr("for", forr), children)...)
+	return g.El("label", g.Attr("for", forr), g.Group(children))
 }
 
 // Option returns an element with name "option", the given text content and value attribute, and the given children.
 func Option(text, value string, children ...g.Node) g.NodeFunc {
-	return g.El("option", prepend2(g.Attr("value", value), g.Text(text), children)...)
+	return g.El("option", g.Attr("value", value), g.Text(text), g.Group(children))
 }
 
 // Progress returns an element with name "progress", the given value and max attributes, and the given children.
 func Progress(value, max float64, children ...g.Node) g.NodeFunc {
-	return g.El("progress", prepend2(
+	return g.El("progress",
 		g.Attr("value", fmt.Sprintf("%v", value)),
 		g.Attr("max", fmt.Sprintf("%v", max)),
-		children)...)
+		g.Group(children))
 }
 
 // Select returns an element with name "select", the given name attribute, and the given children.
 func Select(name string, children ...g.Node) g.NodeFunc {
-	return g.El("select", prepend(g.Attr("name", name), children)...)
+	return g.El("select", g.Attr("name", name), g.Group(children))
 }
 
 // Textarea returns an element with name "textarea", the given name attribute, and the given children.
 func Textarea(name string, children ...g.Node) g.NodeFunc {
-	return g.El("textarea", prepend(g.Attr("name", name), children)...)
+	return g.El("textarea", g.Attr("name", name), g.Group(children))
 }
diff --git a/el/inline.go b/el/inline.go
index f9ae39f..e0530d3 100644
--- a/el/inline.go
+++ b/el/inline.go
@@ -9,21 +9,21 @@ func Span(children ...g.Node) g.NodeFunc {
 }
 
 func A(href string, children ...g.Node) g.NodeFunc {
-	return g.El("a", prepend(g.Attr("href", href), children)...)
+	return g.El("a", g.Attr("href", href), g.Group(children))
 }
 
 func B(text string, children ...g.Node) g.NodeFunc {
-	return g.El("b", prepend(g.Text(text), children)...)
+	return g.El("b", g.Text(text), g.Group(children))
 }
 
 func Strong(text string, children ...g.Node) g.NodeFunc {
-	return g.El("strong", prepend(g.Text(text), children)...)
+	return g.El("strong", g.Text(text), g.Group(children))
 }
 
 func I(text string, children ...g.Node) g.NodeFunc {
-	return g.El("i", prepend(g.Text(text), children)...)
+	return g.El("i", g.Text(text), g.Group(children))
 }
 
 func Em(text string, children ...g.Node) g.NodeFunc {
-	return g.El("em", prepend(g.Text(text), children)...)
+	return g.El("em", g.Text(text), g.Group(children))
 }
diff --git a/el/media.go b/el/media.go
index 4a19865..89787da 100644
--- a/el/media.go
+++ b/el/media.go
@@ -5,5 +5,5 @@ import (
 )
 
 func Img(src, alt string, children ...g.Node) g.NodeFunc {
-	return g.El("img", prepend2(g.Attr("src", src), g.Attr("alt", alt), children)...)
+	return g.El("img", g.Attr("src", src), g.Attr("alt", alt), g.Group(children))
 }
diff --git a/el/section.go b/el/section.go
index 3c31ebc..f49a39c 100644
--- a/el/section.go
+++ b/el/section.go
@@ -31,32 +31,32 @@ func Header(children ...g.Node) g.NodeFunc {
 
 // H1 returns an element with name "h1", the given text content, and the given children.
 func H1(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h1", prepend(g.Text(text), children)...)
+	return g.El("h1", g.Text(text), g.Group(children))
 }
 
 // H2 returns an element with name "h2", the given text content, and the given children.
 func H2(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h2", prepend(g.Text(text), children)...)
+	return g.El("h2", g.Text(text), g.Group(children))
 }
 
 // H3 returns an element with name "h3", the given text content, and the given children.
 func H3(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h3", prepend(g.Text(text), children)...)
+	return g.El("h3", g.Text(text), g.Group(children))
 }
 
 // H4 returns an element with name "h4", the given text content, and the given children.
 func H4(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h4", prepend(g.Text(text), children)...)
+	return g.El("h4", g.Text(text), g.Group(children))
 }
 
 // H5 returns an element with name "h5", the given text content, and the given children.
 func H5(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h5", prepend(g.Text(text), children)...)
+	return g.El("h5", g.Text(text), g.Group(children))
 }
 
 // H6 returns an element with name "h6", the given text content, and the given children.
 func H6(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h6", prepend(g.Text(text), children)...)
+	return g.El("h6", g.Text(text), g.Group(children))
 }
 
 // HGroup returns an element with name "hgroup" and the given children.
diff --git a/gomponents.go b/gomponents.go
index 52d5355..c0aa510 100644
--- a/gomponents.go
+++ b/gomponents.go
@@ -64,17 +64,7 @@ func El(name string, children ...Node) NodeFunc {
 		}
 
 		for _, c := range children {
-			if p, ok := c.(Placer); ok {
-				switch p.Place() {
-				case Inside:
-					inside.WriteString(c.Render())
-				case Outside:
-					outside.WriteString(c.Render())
-				}
-				continue
-			}
-			// If c doesn't implement Placer, default to outside
-			outside.WriteString(c.Render())
+			renderChild(c, &inside, &outside)
 		}
 
 		b.WriteString(inside.String())
@@ -93,6 +83,26 @@ func El(name string, children ...Node) NodeFunc {
 	}
 }
 
+func renderChild(c Node, inside, outside *strings.Builder) {
+	if g, ok := c.(group); ok {
+		for _, groupC := range g.children {
+			renderChild(groupC, inside, outside)
+		}
+		return
+	}
+	if p, ok := c.(Placer); ok {
+		switch p.Place() {
+		case Inside:
+			inside.WriteString(c.Render())
+		case Outside:
+			outside.WriteString(c.Render())
+		}
+		return
+	}
+	// If c doesn't implement Placer, default to outside
+	outside.WriteString(c.Render())
+}
+
 // Attr creates an attr DOM Node.
 // If one parameter is passed, it's a name-only attribute (like "required").
 // If two parameters are passed, it's a name-value attribute (like `class="header"`).
@@ -156,3 +166,18 @@ func Write(w io.Writer, n Node) error {
 	_, err := w.Write([]byte(n.Render()))
 	return err
 }
+
+type group struct {
+	children []Node
+}
+
+func (g group) Render() string {
+	panic("cannot render group")
+}
+
+// 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.
+func Group(children []Node) Node {
+	return group{children: children}
+}


Test patch:
diff --git a/gomponents_test.go b/gomponents_test.go
index 0e397cb..db9256c 100644
--- a/gomponents_test.go
+++ b/gomponents_test.go
@@ -133,3 +133,25 @@ func TestWrite(t *testing.T) {
 		}
 	})
 }
+
+func TestGroup(t *testing.T) {
+	t.Run("groups multiple nodes into one", func(t *testing.T) {
+		children := []g.Node{g.El("div", g.Attr("id", "hat")), g.El("div")}
+		e := g.El("div", g.Attr("class", "foo"), g.El("div"), g.Group(children))
+		assert.Equal(t, `<div class="foo"><div /><div id="hat" /><div /></div>`, e)
+	})
+
+	t.Run("panics on direct render", func(t *testing.T) {
+		e := g.Group(nil)
+		panicced := false
+		defer func() {
+			if err := recover(); err != nil {
+				panicced = true
+			}
+		}()
+		e.Render()
+		if !panicced {
+			t.FailNow()
+		}
+	})
+}