Repository: maragudk/gomponents
Pull request #35 (2020-10-28 15:45:36)

Add attribute helpers
Also refactor tests to be table-driven, for readability.

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

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

```diff
@@            Coverage Diff            @@
##            master       #35   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            6         7    +1     
  Lines          176       207   +31     
=========================================
+ Hits           176       207   +31     
```


| [Impacted Files](https://codecov.io/gh/maragudk/gomponents/pull/35?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [attr/attributes.go](https://codecov.io/gh/maragudk/gomponents/pull/35/diff?src=pr&el=tree#diff-YXR0ci9hdHRyaWJ1dGVzLmdv) | `100.00% <ø> (ø)` | |
| [attr/boolean.go](https://codecov.io/gh/maragudk/gomponents/pull/35/diff?src=pr&el=tree#diff-YXR0ci9ib29sZWFuLmdv) | `100.00% <100.00%> (ø)` | |
| [attr/simple.go](https://codecov.io/gh/maragudk/gomponents/pull/35/diff?src=pr&el=tree#diff-YXR0ci9zaW1wbGUuZ28=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/maragudk/gomponents/pull/35?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/35?src=pr&el=footer). Last update [18e9033...d8385e6](https://codecov.io/gh/maragudk/gomponents/pull/35?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 415e6f1..2b25681 100644
--- a/attr/attributes.go
+++ b/attr/attributes.go
@@ -9,16 +9,6 @@ import (
 	g "github.com/maragudk/gomponents"
 )
 
-// ID returns an attribute with name "id" and the given value.
-func ID(v string) g.Node {
-	return g.Attr("id", v)
-}
-
-// Class returns an attribute with name "class" and the given value.
-func Class(v string) g.Node {
-	return g.Attr("class", v)
-}
-
 // Classes is a map of strings to booleans, which Renders to an attribute with name "class".
 // The attribute value is a sorted, space-separated string of all the map keys,
 // for which the corresponding map value is true.
diff --git a/attr/boolean.go b/attr/boolean.go
new file mode 100644
index 0000000..2a70c9d
--- /dev/null
+++ b/attr/boolean.go
@@ -0,0 +1,45 @@
+package attr
+
+import (
+	g "github.com/maragudk/gomponents"
+)
+
+func Async() g.Node {
+	return g.Attr("async")
+}
+
+func AutoFocus() g.Node {
+	return g.Attr("autofocus")
+}
+
+func AutoPlay() g.Node {
+	return g.Attr("autoplay")
+}
+
+func Controls() g.Node {
+	return g.Attr("controls")
+}
+
+func Defer() g.Node {
+	return g.Attr("defer")
+}
+
+func Disabled() g.Node {
+	return g.Attr("disabled")
+}
+
+func Multiple() g.Node {
+	return g.Attr("multiple")
+}
+
+func ReadOnly() g.Node {
+	return g.Attr("readonly")
+}
+
+func Required() g.Node {
+	return g.Attr("required")
+}
+
+func Selected() g.Node {
+	return g.Attr("selected")
+}
diff --git a/attr/form.go b/attr/form.go
deleted file mode 100644
index b3eec05..0000000
--- a/attr/form.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package attr
-
-import (
-	g "github.com/maragudk/gomponents"
-)
-
-// Placeholder returns an attribute with name "placeholder" and the given value.
-func Placeholder(v string) g.Node {
-	return g.Attr("placeholder", v)
-}
-
-// Required returns an attribute with name "required".
-func Required() g.Node {
-	return g.Attr("required")
-}
diff --git a/attr/simple.go b/attr/simple.go
new file mode 100644
index 0000000..e286c26
--- /dev/null
+++ b/attr/simple.go
@@ -0,0 +1,105 @@
+package attr
+
+import (
+	g "github.com/maragudk/gomponents"
+)
+
+func Accept(v string) g.Node {
+	return g.Attr("accept", v)
+}
+
+func AutoComplete(v string) g.Node {
+	return g.Attr("autocomplete", v)
+}
+
+func Class(v string) g.Node {
+	return g.Attr("class", v)
+}
+
+func Form(v string) g.Node {
+	return g.Attr("form", v)
+}
+
+func Height(v string) g.Node {
+	return g.Attr("height", v)
+}
+
+func Href(v string) g.Node {
+	return g.Attr("href", v)
+}
+
+func ID(v string) g.Node {
+	return g.Attr("id", v)
+}
+
+func Lang(v string) g.Node {
+	return g.Attr("lang", v)
+}
+
+func Max(v string) g.Node {
+	return g.Attr("max", v)
+}
+
+func MaxLength(v string) g.Node {
+	return g.Attr("maxlength", v)
+}
+
+func Min(v string) g.Node {
+	return g.Attr("min", v)
+}
+
+func MinLength(v string) g.Node {
+	return g.Attr("minlength", v)
+}
+
+func Name(v string) g.Node {
+	return g.Attr("name", v)
+}
+
+func Pattern(v string) g.Node {
+	return g.Attr("pattern", v)
+}
+
+func Preload(v string) g.Node {
+	return g.Attr("preload", v)
+}
+
+func Placeholder(v string) g.Node {
+	return g.Attr("placeholder", v)
+}
+
+func Rel(v string) g.Node {
+	return g.Attr("rel", v)
+}
+
+func Src(v string) g.Node {
+	return g.Attr("src", v)
+}
+
+func Style(v string) g.Node {
+	return g.Attr("style", v)
+}
+
+func TabIndex(v string) g.Node {
+	return g.Attr("tabindex", v)
+}
+
+func Target(v string) g.Node {
+	return g.Attr("target", v)
+}
+
+func Title(v string) g.Node {
+	return g.Attr("title", v)
+}
+
+func Type(v string) g.Node {
+	return g.Attr("type", v)
+}
+
+func Value(v string) g.Node {
+	return g.Attr("value", v)
+}
+
+func Width(v string) g.Node {
+	return g.Attr("width", v)
+}
diff --git a/examples/simple/simple.go b/examples/simple/simple.go
index 56747e4..6683bc4 100644
--- a/examples/simple/simple.go
+++ b/examples/simple/simple.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"fmt"
 	"net/http"
 	"time"
 
@@ -11,18 +10,14 @@ import (
 )
 
 func main() {
-	_ = http.ListenAndServe("localhost:8080", handler())
+	_ = http.ListenAndServe("localhost:8080", http.HandlerFunc(handler))
 }
 
-func handler() http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		props := pageProps{
-			title: r.URL.Path,
-			path:  r.URL.Path,
-		}
-
-		_ = g.Write(w, page(props))
-	}
+func handler(w http.ResponseWriter, r *http.Request) {
+	_ = g.Write(w, page(pageProps{
+		title: r.URL.Path,
+		path:  r.URL.Path,
+	}))
 }
 
 type pageProps struct {
@@ -32,11 +27,10 @@ type pageProps struct {
 
 func page(props pageProps) g.Node {
 	return el.Document(
-		el.HTML(
-			g.Attr("lang", "en"),
+		el.HTML(attr.Lang("en"),
 			el.Head(
 				el.Title(props.title),
-				el.Style(g.Attr("type", "text/css"),
+				el.Style(attr.Type("text/css"),
 					g.Raw(".is-active{font-weight: bold}"),
 					g.Raw("ul.nav { list-style-type: none; margin: 0; padding: 0; overflow: hidden; }"),
 					g.Raw("ul.nav li { display: block;  padding: 8px; float: left; }"),
@@ -46,8 +40,8 @@ func page(props pageProps) g.Node {
 				navbar(navbarProps{path: props.path}),
 				el.Hr(),
 				el.H1(props.title),
-				el.P(g.Text(fmt.Sprintf("Welcome to the page at %v.", props.path))),
-				el.P(g.Text(fmt.Sprintf("Rendered at %v", time.Now()))),
+				el.P(g.Textf("Welcome to the page at %v.", props.path)),
+				el.P(g.Textf("Rendered at %v", time.Now())),
 			),
 		),
 	)
@@ -68,8 +62,9 @@ func navbar(props navbarProps) g.Node {
 	}
 	var lis []g.Node
 	for _, item := range items {
-		lis = append(lis, el.Li(
-			el.A(item.path, attr.Classes(map[string]bool{"is-active": props.path == item.path}), g.Text(item.text))))
+		lis = append(lis, el.Li(el.A(item.path,
+			attr.Classes(map[string]bool{"is-active": props.path == item.path}),
+			g.Text(item.text))))
 	}
-	return el.Ul(append(lis, attr.Class("nav"))...)
+	return el.Ul(attr.Class("nav"), g.Group(lis))
 }


Test patch:
diff --git a/attr/attributes_test.go b/attr/attributes_test.go
index c2ddff0..efb7044 100644
--- a/attr/attributes_test.go
+++ b/attr/attributes_test.go
@@ -8,18 +8,6 @@ import (
 	"github.com/maragudk/gomponents/attr"
 )
 
-func TestID(t *testing.T) {
-	t.Run("given a value, returns id=value", func(t *testing.T) {
-		assert.Equal(t, ` id="hat"`, attr.ID("hat"))
-	})
-}
-
-func TestClass(t *testing.T) {
-	t.Run("given a value, returns class=value", func(t *testing.T) {
-		assert.Equal(t, ` class="hat"`, attr.Class("hat"))
-	})
-}
-
 func TestClasses(t *testing.T) {
 	t.Run("given a map, returns sorted keys from the map with value true", func(t *testing.T) {
 		assert.Equal(t, ` class="boheme-hat hat partyhat"`, attr.Classes{
diff --git a/attr/boolean_test.go b/attr/boolean_test.go
new file mode 100644
index 0000000..4e6bb37
--- /dev/null
+++ b/attr/boolean_test.go
@@ -0,0 +1,32 @@
+package attr_test
+
+import (
+	"fmt"
+	"testing"
+
+	g "github.com/maragudk/gomponents"
+	"github.com/maragudk/gomponents/assert"
+	"github.com/maragudk/gomponents/attr"
+)
+
+func TestBooleanAttributes(t *testing.T) {
+	cases := map[string]func() g.Node{
+		"async":     attr.Async,
+		"autofocus": attr.AutoFocus,
+		"autoplay":  attr.AutoPlay,
+		"controls":  attr.Controls,
+		"defer":     attr.Defer,
+		"disabled":  attr.Disabled,
+		"multiple":  attr.Multiple,
+		"readonly":  attr.ReadOnly,
+		"required":  attr.Required,
+		"selected":  attr.Selected,
+	}
+
+	for name, fn := range cases {
+		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
+			n := g.El("div", fn())
+			assert.Equal(t, fmt.Sprintf(`<div %v />`, name), n)
+		})
+	}
+}
diff --git a/attr/form_test.go b/attr/form_test.go
deleted file mode 100644
index d222464..0000000
--- a/attr/form_test.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package attr_test
-
-import (
-	"testing"
-
-	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/assert"
-	"github.com/maragudk/gomponents/attr"
-)
-
-func TestForms(t *testing.T) {
-	t.Run("adds placeholder and required attributes", func(t *testing.T) {
-		e := g.El("input", attr.Placeholder("hat"), attr.Required())
-		assert.Equal(t, `<input placeholder="hat" required />`, e)
-	})
-}
diff --git a/attr/simple_test.go b/attr/simple_test.go
new file mode 100644
index 0000000..784561a
--- /dev/null
+++ b/attr/simple_test.go
@@ -0,0 +1,47 @@
+package attr_test
+
+import (
+	"fmt"
+	"testing"
+
+	g "github.com/maragudk/gomponents"
+	"github.com/maragudk/gomponents/assert"
+	"github.com/maragudk/gomponents/attr"
+)
+
+func TestSimpleAttributes(t *testing.T) {
+	cases := map[string]func(string) g.Node{
+		"accept":       attr.Accept,
+		"autocomplete": attr.AutoComplete,
+		"class":        attr.Class,
+		"form":         attr.Form,
+		"height":       attr.Height,
+		"href":         attr.Href,
+		"id":           attr.ID,
+		"lang":         attr.Lang,
+		"max":          attr.Max,
+		"maxlength":    attr.MaxLength,
+		"min":          attr.Min,
+		"minlength":    attr.MinLength,
+		"name":         attr.Name,
+		"pattern":      attr.Pattern,
+		"preload":      attr.Preload,
+		"placeholder":  attr.Placeholder,
+		"rel":          attr.Rel,
+		"src":          attr.Src,
+		"style":        attr.Style,
+		"tabindex":     attr.TabIndex,
+		"target":       attr.Target,
+		"title":        attr.Title,
+		"type":         attr.Type,
+		"value":        attr.Value,
+		"width":        attr.Width,
+	}
+
+	for name, fn := range cases {
+		t.Run(fmt.Sprintf(`should output %v="hat"`, name), func(t *testing.T) {
+			n := g.El("div", fn("hat"))
+			assert.Equal(t, fmt.Sprintf(`<div %v="hat" />`, name), n)
+		})
+	}
+}