Repository: maragudk/gomponents
Pull request #18 (2020-09-24 11:17:24)

Introduce Placer interface
When implemented, the `Place` method of the `Placer` interface tells `Render` in `El` where to put a Node. This is relevant for helpers that want to be rendered like attributes, inside the parent element.

Fixes the bug where `attr.Classes` was rendered outside the element.

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

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

```diff
@@            Coverage Diff            @@
##            master       #18   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            3         3           
  Lines           85        92    +7     
=========================================
+ Hits            85        92    +7     
```


| [Impacted Files](https://codecov.io/gh/maragudk/gomponents/pull/18?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [attr/attributes.go](https://codecov.io/gh/maragudk/gomponents/pull/18/diff?src=pr&el=tree#diff-YXR0ci9hdHRyaWJ1dGVzLmdv) | `100.00% <100.00%> (ø)` | |
| [gomponents.go](https://codecov.io/gh/maragudk/gomponents/pull/18/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/18?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/18?src=pr&el=footer). Last update [f27cb0c...d765645](https://codecov.io/gh/maragudk/gomponents/pull/18?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/attr/attributes.go b/attr/attributes.go
index 56fcd74..415e6f1 100644
--- a/attr/attributes.go
+++ b/attr/attributes.go
@@ -35,6 +35,10 @@ func (c Classes) Render() string {
 	return g.Attr("class", strings.Join(included, " ")).Render()
 }
 
+func (c Classes) Place() g.Placement {
+	return g.Inside
+}
+
 // String satisfies fmt.Stringer.
 func (c Classes) String() string {
 	return c.Render()
diff --git a/gomponents.go b/gomponents.go
index 21d3e75..73d0545 100644
--- a/gomponents.go
+++ b/gomponents.go
@@ -19,6 +19,20 @@ type Node interface {
 	Render() string
 }
 
+// Placer can be implemented to tell Render functions where to place the string representation of a Node
+// in the parent element.
+type Placer interface {
+	Place() Placement
+}
+
+// Placement is used with the Placer interface.
+type Placement int
+
+const (
+	Outside = Placement(iota)
+	Inside
+)
+
 // NodeFunc is render function that is also a Node.
 type NodeFunc func() string
 
@@ -26,6 +40,10 @@ func (n NodeFunc) Render() string {
 	return n()
 }
 
+func (n NodeFunc) Place() Placement {
+	return Outside
+}
+
 // String satisfies fmt.Stringer.
 func (n NodeFunc) String() string {
 	return n.Render()
@@ -35,7 +53,7 @@ func (n NodeFunc) String() string {
 // Use this if no convenience creator exists.
 func El(name string, children ...Node) NodeFunc {
 	return func() string {
-		var b, attrString, childrenString strings.Builder
+		var b, inside, outside strings.Builder
 
 		b.WriteString("<")
 		b.WriteString(name)
@@ -46,22 +64,28 @@ func El(name string, children ...Node) NodeFunc {
 		}
 
 		for _, c := range children {
-			if _, ok := c.(attr); ok {
-				attrString.WriteString(c.Render())
-			} else {
-				childrenString.WriteString(c.Render())
+			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())
 		}
 
-		b.WriteString(attrString.String())
+		b.WriteString(inside.String())
 
-		if childrenString.Len() == 0 {
+		if outside.Len() == 0 {
 			b.WriteString("/>")
 			return b.String()
 		}
 
 		b.WriteString(">")
-		b.WriteString(childrenString.String())
+		b.WriteString(outside.String())
 		b.WriteString("</")
 		b.WriteString(name)
 		b.WriteString(">")
@@ -97,6 +121,10 @@ func (a attr) Render() string {
 	return fmt.Sprintf(` %v="%v"`, a.name, *a.value)
 }
 
+func (a attr) Place() Placement {
+	return Inside
+}
+
 // String satisfies fmt.Stringer.
 func (a attr) String() string {
 	return a.Render()


Test patch:
diff --git a/attr/attributes_test.go b/attr/attributes_test.go
index 9c7ece7..4da28e3 100644
--- a/attr/attributes_test.go
+++ b/attr/attributes_test.go
@@ -3,6 +3,7 @@ package attr_test
 import (
 	"testing"
 
+	g "github.com/maragudk/gomponents"
 	"github.com/maragudk/gomponents/assert"
 	"github.com/maragudk/gomponents/attr"
 )
@@ -29,6 +30,11 @@ func TestClasses(t *testing.T) {
 		})
 	})
 
+	t.Run("renders as attribute in an element", func(t *testing.T) {
+		e := g.El("div", attr.Classes{"hat": true})
+		assert.Equal(t, `<div class="hat"/>`, e)
+	})
+
 	t.Run("also works with fmt", func(t *testing.T) {
 		a := attr.Classes{"hat": true}
 		if a.String() != ` class="hat"` {
diff --git a/gomponents_test.go b/gomponents_test.go
index ee0b91d..65b82b5 100644
--- a/gomponents_test.go
+++ b/gomponents_test.go
@@ -52,6 +52,12 @@ func TestAttr(t *testing.T) {
 	})
 }
 
+type outsider struct{}
+
+func (o outsider) Render() string {
+	return "outsider"
+}
+
 func TestEl(t *testing.T) {
 	t.Run("renders an empty element if no children given", func(t *testing.T) {
 		e := g.El("div")
@@ -72,6 +78,11 @@ func TestEl(t *testing.T) {
 		e := g.El("div", g.El("span"), g.Attr("class", "hat"))
 		assert.Equal(t, `<div class="hat"><span/></div>`, e)
 	})
+
+	t.Run("renders outside if node does not implement placer", func(t *testing.T) {
+		e := g.El("div", outsider{})
+		assert.Equal(t, `<div>outsider</div>`, e)
+	})
 }
 
 func TestText(t *testing.T) {