Repository: maragudk/gomponents
Pull request #174 (2024-06-06 09:56:37)

Deprecate DataAttr, StyleAttr, TitleAttr, FormEl
This change addresses #170 by deprecating some HTML helpers in favor of using one of the styles as a main one, selected based on what I think is the main use case.

- For `Data`, it's the attribute. I don't see much use of the `<data>` element in the wild.
- For `Style`, it's the attribute. The `style` attribute is everywhere, the `<style>` element is perhaps less so (but not much). This was the hardest one to decide.
- For `Title`, it's the attribute. The `<title>` element only shows up once per document.
- For `Form`, it's the element. I haven't seen much use of the `form` attribute in the wild.

I know this is arguably not a "consistent" approach, but I think it makes for a much nicer API, simply because the most-used option will not be a suffixed version.


Patch:
diff --git a/README.md b/README.md
index ca84c7f..bef2fbf 100644
--- a/README.md
+++ b/README.md
@@ -146,9 +146,10 @@ For more complete examples, see [the examples directory](examples/).
 ### What's up with the specially named elements and attributes?
 
 Unfortunately, there are four main name clashes in HTML elements and attributes, so they need an `El` or `Attr` suffix,
-respectively, to be able to co-exist in the same package in Go:
+to be able to co-exist in the same package in Go. I've chosen one or the other based on what I think is the common usage.
+In either case, the less-used variant also exists in the codebase:
 
-- `data` (`DataEl`/`DataAttr`)
-- `form` (`FormEl`/`FormAttr`)
-- `style` (`StyleEl`/`StyleAttr`)
-- `title` (`TitleEl`/`TitleAttr`)
+- `data` (`DataEl`/`Data`, `DataAttr` also exists)
+- `form` (`Form`/`FormAttr`, `FormEl` also exists)
+- `style` (`StyleEl`/`Style`, `StyleAttr` also exists)
+- `title` (`TitleEl`/`Title`, `TitleAttr` also exists)
diff --git a/html/attributes.go b/html/attributes.go
index 8988ab3..0bc5268 100644
--- a/html/attributes.go
+++ b/html/attributes.go
@@ -109,7 +109,14 @@ func Content(v string) g.Node {
 	return g.Attr("content", v)
 }
 
+// Data attributes automatically have their name prefixed with "data-".
+func Data(name, v string) g.Node {
+	return g.Attr("data-"+name, v)
+}
+
 // DataAttr attributes automatically have their name prefixed with "data-".
+//
+// Deprecated: Use [Data] instead.
 func DataAttr(name, v string) g.Node {
 	return g.Attr("data-"+name, v)
 }
@@ -214,6 +221,11 @@ func Step(v string) g.Node {
 	return g.Attr("step", v)
 }
 
+func Style(v string) g.Node {
+	return g.Attr("style", v)
+}
+
+// Deprecated: Use [Style] instead.
 func StyleAttr(v string) g.Node {
 	return g.Attr("style", v)
 }
@@ -226,6 +238,11 @@ func Target(v string) g.Node {
 	return g.Attr("target", v)
 }
 
+func Title(v string) g.Node {
+	return g.Attr("title", v)
+}
+
+// Deprecated: Use [Title] instead.
 func TitleAttr(v string) g.Node {
 	return g.Attr("title", v)
 }
diff --git a/html/elements.go b/html/elements.go
index 79db67f..eb6fbc2 100644
--- a/html/elements.go
+++ b/html/elements.go
@@ -111,6 +111,11 @@ func Embed(children ...g.Node) g.Node {
 	return g.El("embed", children...)
 }
 
+func Form(children ...g.Node) g.Node {
+	return g.El("form", children...)
+}
+
+// Deprecated: Use [Form] instead.
 func FormEl(children ...g.Node) g.Node {
 	return g.El("form", children...)
 }


Test patch:
diff --git a/html/attributes_test.go b/html/attributes_test.go
index 1478b61..1801428 100644
--- a/html/attributes_test.go
+++ b/html/attributes_test.go
@@ -10,83 +10,91 @@ import (
 )
 
 func TestBooleanAttributes(t *testing.T) {
-	cases := map[string]func() g.Node{
-		"async":       Async,
-		"autofocus":   AutoFocus,
-		"autoplay":    AutoPlay,
-		"checked":     Checked,
-		"controls":    Controls,
-		"defer":       Defer,
-		"disabled":    Disabled,
-		"loop":        Loop,
-		"multiple":    Multiple,
-		"muted":       Muted,
-		"playsinline": PlaysInline,
-		"readonly":    ReadOnly,
-		"required":    Required,
-		"selected":    Selected,
+	tests := []struct {
+		Name string
+		Func func() g.Node
+	}{
+		{Name: "async", Func: Async},
+		{Name: "autofocus", Func: AutoFocus},
+		{Name: "autoplay", Func: AutoPlay},
+		{Name: "checked", Func: Checked},
+		{Name: "controls", Func: Controls},
+		{Name: "defer", Func: Defer},
+		{Name: "disabled", Func: Disabled},
+		{Name: "loop", Func: Loop},
+		{Name: "multiple", Func: Multiple},
+		{Name: "muted", Func: Muted},
+		{Name: "playsinline", Func: PlaysInline},
+		{Name: "readonly", Func: ReadOnly},
+		{Name: "required", Func: Required},
+		{Name: "selected", Func: 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></div>`, name), n)
+	for _, test := range tests {
+		t.Run(test.Name, func(t *testing.T) {
+			n := g.El("div", test.Func())
+			assert.Equal(t, fmt.Sprintf(`<div %v></div>`, test.Name), n)
 		})
 	}
 }
 
 func TestSimpleAttributes(t *testing.T) {
-	cases := map[string]func(string) g.Node{
-		"accept":       Accept,
-		"action":       Action,
-		"alt":          Alt,
-		"as":           As,
-		"autocomplete": AutoComplete,
-		"charset":      Charset,
-		"class":        Class,
-		"cols":         Cols,
-		"colspan":      ColSpan,
-		"content":      Content,
-		"crossorigin":  CrossOrigin,
-		"enctype":      EncType,
-		"for":          For,
-		"form":         FormAttr,
-		"height":       Height,
-		"href":         Href,
-		"id":           ID,
-		"integrity":    Integrity,
-		"lang":         Lang,
-		"loading":      Loading,
-		"max":          Max,
-		"maxlength":    MaxLength,
-		"method":       Method,
-		"min":          Min,
-		"minlength":    MinLength,
-		"name":         Name,
-		"pattern":      Pattern,
-		"placeholder":  Placeholder,
-		"poster":       Poster,
-		"preload":      Preload,
-		"rel":          Rel,
-		"role":         Role,
-		"rows":         Rows,
-		"rowspan":      RowSpan,
-		"src":          Src,
-		"srcset":       SrcSet,
-		"step":         Step,
-		"style":        StyleAttr,
-		"tabindex":     TabIndex,
-		"target":       Target,
-		"title":        TitleAttr,
-		"type":         Type,
-		"value":        Value,
-		"width":        Width,
+	tests := []struct {
+		Name string
+		Func func(string) g.Node
+	}{
+		{Name: "accept", Func: Accept},
+		{Name: "action", Func: Action},
+		{Name: "alt", Func: Alt},
+		{Name: "as", Func: As},
+		{Name: "autocomplete", Func: AutoComplete},
+		{Name: "charset", Func: Charset},
+		{Name: "class", Func: Class},
+		{Name: "cols", Func: Cols},
+		{Name: "colspan", Func: ColSpan},
+		{Name: "content", Func: Content},
+		{Name: "crossorigin", Func: CrossOrigin},
+		{Name: "enctype", Func: EncType},
+		{Name: "for", Func: For},
+		{Name: "form", Func: FormAttr},
+		{Name: "height", Func: Height},
+		{Name: "href", Func: Href},
+		{Name: "id", Func: ID},
+		{Name: "integrity", Func: Integrity},
+		{Name: "lang", Func: Lang},
+		{Name: "loading", Func: Loading},
+		{Name: "max", Func: Max},
+		{Name: "maxlength", Func: MaxLength},
+		{Name: "method", Func: Method},
+		{Name: "min", Func: Min},
+		{Name: "minlength", Func: MinLength},
+		{Name: "name", Func: Name},
+		{Name: "pattern", Func: Pattern},
+		{Name: "placeholder", Func: Placeholder},
+		{Name: "poster", Func: Poster},
+		{Name: "preload", Func: Preload},
+		{Name: "rel", Func: Rel},
+		{Name: "role", Func: Role},
+		{Name: "rows", Func: Rows},
+		{Name: "rowspan", Func: RowSpan},
+		{Name: "src", Func: Src},
+		{Name: "srcset", Func: SrcSet},
+		{Name: "step", Func: Step},
+		{Name: "style", Func: Style},
+		{Name: "style", Func: StyleAttr},
+		{Name: "tabindex", Func: TabIndex},
+		{Name: "target", Func: Target},
+		{Name: "title", Func: Title},
+		{Name: "title", Func: TitleAttr},
+		{Name: "type", Func: Type},
+		{Name: "value", Func: Value},
+		{Name: "width", Func: 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"></div>`, name), n)
+	for _, test := range tests {
+		t.Run(test.Name, func(t *testing.T) {
+			n := g.El("div", test.Func("hat"))
+			assert.Equal(t, fmt.Sprintf(`<div %v="hat"></div>`, test.Name), n)
 		})
 	}
 }
@@ -98,9 +106,12 @@ func TestAria(t *testing.T) {
 	})
 }
 
-func TestDataAttr(t *testing.T) {
+func TestData(t *testing.T) {
 	t.Run("returns an attribute which name is prefixed with data-", func(t *testing.T) {
-		n := DataAttr("id", "partyhat")
+		n := Data("id", "partyhat")
+		assert.Equal(t, ` data-id="partyhat"`, n)
+
+		n = DataAttr("id", "partyhat")
 		assert.Equal(t, ` data-id="partyhat"`, n)
 	})
 }
diff --git a/html/elements_test.go b/html/elements_test.go
index cced400..ffb225a 100644
--- a/html/elements_test.go
+++ b/html/elements_test.go
@@ -28,128 +28,135 @@ func TestDoctype(t *testing.T) {
 }
 
 func TestSimpleElements(t *testing.T) {
-	cases := map[string]func(...g.Node) g.Node{
-		"a":          A,
-		"abbr":       Abbr,
-		"address":    Address,
-		"article":    Article,
-		"aside":      Aside,
-		"audio":      Audio,
-		"b":          B,
-		"blockquote": BlockQuote,
-		"body":       Body,
-		"button":     Button,
-		"canvas":     Canvas,
-		"caption":    Caption,
-		"cite":       Cite,
-		"code":       Code,
-		"colgroup":   ColGroup,
-		"data":       DataEl,
-		"datalist":   DataList,
-		"dd":         Dd,
-		"del":        Del,
-		"details":    Details,
-		"dfn":        Dfn,
-		"dialog":     Dialog,
-		"div":        Div,
-		"dl":         Dl,
-		"dt":         Dt,
-		"em":         Em,
-		"fieldset":   FieldSet,
-		"figcaption": FigCaption,
-		"figure":     Figure,
-		"footer":     Footer,
-		"form":       FormEl,
-		"h1":         H1,
-		"h2":         H2,
-		"h3":         H3,
-		"h4":         H4,
-		"h5":         H5,
-		"h6":         H6,
-		"head":       Head,
-		"header":     Header,
-		"hgroup":     HGroup,
-		"html":       HTML,
-		"i":          I,
-		"iframe":     IFrame,
-		"ins":        Ins,
-		"kbd":        Kbd,
-		"label":      Label,
-		"legend":     Legend,
-		"li":         Li,
-		"main":       Main,
-		"mark":       Mark,
-		"menu":       Menu,
-		"meter":      Meter,
-		"nav":        Nav,
-		"noscript":   NoScript,
-		"object":     Object,
-		"ol":         Ol,
-		"optgroup":   OptGroup,
-		"option":     Option,
-		"p":          P,
-		"picture":    Picture,
-		"pre":        Pre,
-		"progress":   Progress,
-		"q":          Q,
-		"s":          S,
-		"samp":       Samp,
-		"script":     Script,
-		"section":    Section,
-		"select":     Select,
-		"small":      Small,
-		"span":       Span,
-		"strong":     Strong,
-		"style":      StyleEl,
-		"sub":        Sub,
-		"summary":    Summary,
-		"sup":        Sup,
-		"svg":        SVG,
-		"table":      Table,
-		"tbody":      TBody,
-		"td":         Td,
-		"textarea":   Textarea,
-		"tfoot":      TFoot,
-		"th":         Th,
-		"thead":      THead,
-		"time":       Time,
-		"title":      TitleEl,
-		"tr":         Tr,
-		"u":          U,
-		"ul":         Ul,
-		"var":        Var,
-		"video":      Video,
+	tests := []struct {
+		Name string
+		Func func(...g.Node) g.Node
+	}{
+		{Name: "a", Func: A},
+		{Name: "abbr", Func: Abbr},
+		{Name: "address", Func: Address},
+		{Name: "article", Func: Article},
+		{Name: "aside", Func: Aside},
+		{Name: "audio", Func: Audio},
+		{Name: "b", Func: B},
+		{Name: "blockquote", Func: BlockQuote},
+		{Name: "body", Func: Body},
+		{Name: "button", Func: Button},
+		{Name: "canvas", Func: Canvas},
+		{Name: "caption", Func: Caption},
+		{Name: "cite", Func: Cite},
+		{Name: "code", Func: Code},
+		{Name: "colgroup", Func: ColGroup},
+		{Name: "data", Func: DataEl},
+		{Name: "datalist", Func: DataList},
+		{Name: "dd", Func: Dd},
+		{Name: "del", Func: Del},
+		{Name: "details", Func: Details},
+		{Name: "dfn", Func: Dfn},
+		{Name: "dialog", Func: Dialog},
+		{Name: "div", Func: Div},
+		{Name: "dl", Func: Dl},
+		{Name: "dt", Func: Dt},
+		{Name: "em", Func: Em},
+		{Name: "fieldset", Func: FieldSet},
+		{Name: "figcaption", Func: FigCaption},
+		{Name: "figure", Func: Figure},
+		{Name: "footer", Func: Footer},
+		{Name: "form", Func: Form},
+		{Name: "form", Func: FormEl},
+		{Name: "h1", Func: H1},
+		{Name: "h2", Func: H2},
+		{Name: "h3", Func: H3},
+		{Name: "h4", Func: H4},
+		{Name: "h5", Func: H5},
+		{Name: "h6", Func: H6},
+		{Name: "head", Func: Head},
+		{Name: "header", Func: Header},
+		{Name: "hgroup", Func: HGroup},
+		{Name: "html", Func: HTML},
+		{Name: "i", Func: I},
+		{Name: "iframe", Func: IFrame},
+		{Name: "ins", Func: Ins},
+		{Name: "kbd", Func: Kbd},
+		{Name: "label", Func: Label},
+		{Name: "legend", Func: Legend},
+		{Name: "li", Func: Li},
+		{Name: "main", Func: Main},
+		{Name: "mark", Func: Mark},
+		{Name: "menu", Func: Menu},
+		{Name: "meter", Func: Meter},
+		{Name: "nav", Func: Nav},
+		{Name: "noscript", Func: NoScript},
+		{Name: "object", Func: Object},
+		{Name: "ol", Func: Ol},
+		{Name: "optgroup", Func: OptGroup},
+		{Name: "option", Func: Option},
+		{Name: "p", Func: P},
+		{Name: "picture", Func: Picture},
+		{Name: "pre", Func: Pre},
+		{Name: "progress", Func: Progress},
+		{Name: "q", Func: Q},
+		{Name: "s", Func: S},
+		{Name: "samp", Func: Samp},
+		{Name: "script", Func: Script},
+		{Name: "section", Func: Section},
+		{Name: "select", Func: Select},
+		{Name: "small", Func: Small},
+		{Name: "span", Func: Span},
+		{Name: "strong", Func: Strong},
+		{Name: "style", Func: StyleEl},
+		{Name: "sub", Func: Sub},
+		{Name: "summary", Func: Summary},
+		{Name: "sup", Func: Sup},
+		{Name: "svg", Func: SVG},
+		{Name: "table", Func: Table},
+		{Name: "tbody", Func: TBody},
+		{Name: "td", Func: Td},
+		{Name: "textarea", Func: Textarea},
+		{Name: "tfoot", Func: TFoot},
+		{Name: "th", Func: Th},
+		{Name: "thead", Func: THead},
+		{Name: "time", Func: Time},
+		{Name: "title", Func: TitleEl},
+		{Name: "tr", Func: Tr},
+		{Name: "u", Func: U},
+		{Name: "ul", Func: Ul},
+		{Name: "var", Func: Var},
+		{Name: "video", Func: Video},
 	}
 
-	for name, fn := range cases {
-		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
-			n := fn(g.Attr("id", "hat"))
-			assert.Equal(t, fmt.Sprintf(`<%v id="hat"></%v>`, name, name), n)
+	for _, test := range tests {
+		t.Run(test.Name, func(t *testing.T) {
+			n := test.Func(g.Attr("id", "hat"))
+			assert.Equal(t, fmt.Sprintf(`<%v id="hat"></%v>`, test.Name, test.Name), n)
 		})
 	}
 }
 
 func TestSimpleVoidKindElements(t *testing.T) {
-	cases := map[string]func(...g.Node) g.Node{
-		"area":   Area,
-		"base":   Base,
-		"br":     Br,
-		"col":    Col,
-		"embed":  Embed,
-		"hr":     Hr,
-		"img":    Img,
-		"input":  Input,
-		"link":   Link,
-		"meta":   Meta,
-		"param":  Param,
-		"source": Source,
-		"wbr":    Wbr,
+	tests := []struct {
+		Name string
+		Func func(...g.Node) g.Node
+	}{
+		{Name: "area", Func: Area},
+		{Name: "base", Func: Base},
+		{Name: "br", Func: Br},
+		{Name: "col", Func: Col},
+		{Name: "embed", Func: Embed},
+		{Name: "hr", Func: Hr},
+		{Name: "img", Func: Img},
+		{Name: "input", Func: Input},
+		{Name: "link", Func: Link},
+		{Name: "meta", Func: Meta},
+		{Name: "param", Func: Param},
+		{Name: "source", Func: Source},
+		{Name: "wbr", Func: Wbr},
 	}
 
-	for name, fn := range cases {
-		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
-			n := fn(g.Attr("id", "hat"))
-			assert.Equal(t, fmt.Sprintf(`<%v id="hat">`, name), n)
+	for _, test := range tests {
+		t.Run(test.Name, func(t *testing.T) {
+			n := test.Func(g.Attr("id", "hat"))
+			assert.Equal(t, fmt.Sprintf(`<%v id="hat">`, test.Name), n)
 		})
 	}
 }