diff options
-rw-r--r-- | README.md | 116 | ||||
-rw-r--r-- | attr/boolean.go | 45 | ||||
-rw-r--r-- | attr/boolean_test.go | 32 | ||||
-rw-r--r-- | attr/simple_test.go | 51 | ||||
-rw-r--r-- | components/attributes.go (renamed from attr/attributes.go) | 7 | ||||
-rw-r--r-- | components/attributes_test.go (renamed from attr/attributes_test.go) | 10 | ||||
-rw-r--r-- | components/documents.go | 29 | ||||
-rw-r--r-- | components/documents_test.go | 13 | ||||
-rw-r--r-- | el/elements.go | 68 | ||||
-rw-r--r-- | el/elements_test.go | 82 | ||||
-rw-r--r-- | el/simple.go | 257 | ||||
-rw-r--r-- | el/simple_test.go | 97 | ||||
-rw-r--r-- | el/text.go | 131 | ||||
-rw-r--r-- | el/text_test.go | 52 | ||||
-rw-r--r-- | examples/dot-import/dot-import.go | 43 | ||||
-rw-r--r-- | examples/simple/simple.go | 30 | ||||
-rw-r--r-- | html/attributes.go (renamed from attr/simple.go) | 48 | ||||
-rw-r--r-- | html/attributes_test.go | 73 | ||||
-rw-r--r-- | html/elements.go | 447 | ||||
-rw-r--r-- | html/elements_test.go | 212 |
20 files changed, 910 insertions, 933 deletions
diff --git a/README.md b/README.md index cba1e66..cd8a887 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,19 @@ gomponents aims to make it easy to build HTML5 pages of reusable components, without the use of a template language. Think server-side-rendered React, but without the virtual DOM and diffing. -The implementation is still incomplete, but usable. The API may change until version 1 is reached. +The implementation is very usable, but the API may change until version 1 is reached. Check out the blog post [gomponents: declarative view components in Go](https://www.maragu.dk/blog/gomponents-declarative-view-components-in-go/) for background. ## Features +- Build reusable view components - Write declarative HTML5 in Go without all the strings, so you get - Type safety - Auto-completion - Nice formatting with `gofmt` -- Simple API that's easy to learn and use -- Build reusable view components +- Simple API that's easy to learn and use (you know most already if you know HTML) - No external dependencies ## Usage @@ -31,7 +31,8 @@ Get the library using `go get`: go get -u github.com/maragudk/gomponents ``` -Then do something like this: +The preferred way to use gomponents is with so-called dot-imports (note the dot before the `gomponents/html` import), +to give you that smooth, native HTML feel: ```go package main @@ -40,47 +41,51 @@ import ( "net/http" g "github.com/maragudk/gomponents" - "github.com/maragudk/gomponents/attr" - "github.com/maragudk/gomponents/el" + c "github.com/maragudk/gomponents/components" + . "github.com/maragudk/gomponents/html" ) 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) { - page := Page("Hi!", r.URL.Path) - _ = page.Render(w) - } +func handler(w http.ResponseWriter, r *http.Request) { + _ = Page("Hi!", r.URL.Path).Render(w) } -func Page(title, path string) g.Node { - return el.Document( - el.HTML( - g.Attr("lang", "en"), - el.Head( - el.Title(title), - el.Style(g.Attr("type", "text/css"), g.Raw(".is-active{font-weight: bold}")), +func Page(title, currentPath string) g.Node { + return Document( + HTML( + Lang("en"), + Head( + TitleEl(title), + StyleEl(Type("text/css"), g.Raw(".is-active{ font-weight: bold }")), ), - el.Body( - Navbar(path), - el.H1(title), - el.P(g.Textf("Welcome to the page at %v.", path)), + Body( + Navbar(currentPath), + H1(title), + P(g.Textf("Welcome to the page at %v.", currentPath)), ), ), ) } -func Navbar(path string) g.Node { - return g.El("nav", - el.A("/", attr.Classes{"is-active": path == "/"}, g.Text("Home")), - el.A("/about", attr.Classes{"is-active": path == "/about"}, g.Text("About")), +func Navbar(currentPath string) g.Node { + return Nav( + NavbarLink("/", "Home", currentPath), + NavbarLink("/about", "About", currentPath), ) } + +func NavbarLink(href, name, currentPath string) g.Node { + return A(href, c.Classes{"is-active": currentPath == href}, g.Text(name)) +} ``` -You could also use a page template to simplify your code a bit: +Some people don't like dot-imports, and luckily it's completely optional. +If you don't like dot-imports, just use regular imports. + +You could also use the provided HTML5 document template to simplify your code a bit: ```go package main @@ -89,41 +94,52 @@ import ( "net/http" g "github.com/maragudk/gomponents" - "github.com/maragudk/gomponents/attr" - c "github.com/maragudk/gomponents/components" - "github.com/maragudk/gomponents/el" + . "github.com/maragudk/gomponents/components" + . "github.com/maragudk/gomponents/html" ) 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) { - page := Page("Hi!", r.URL.Path) - _ = page.Render(w) - } +func handler(w http.ResponseWriter, r *http.Request) { + _ = Page("Hi!", r.URL.Path).Render(w) } -func Page(title, path string) g.Node { - return c.HTML5(c.DocumentProps{ - Title: title, - Language: "en", - Head: []g.Node{el.Style(g.Attr("type", "text/css"), g.Raw(".is-active{font-weight: bold}"))}, - Body: []g.Node{ - Navbar(path), - el.H1(title), - el.P(g.Textf("Welcome to the page at %v.", path)), +func Page(title, currentPath string) g.Node { + return HTML5(HTML5Props{ + Title: title, + Language: "en", + Head: []g.Node{ + StyleEl(Type("text/css"), g.Raw(".is-active{ font-weight: bold }")), + }, + Body: []g.Node{ + Navbar(currentPath), + H1(title), + P(g.Textf("Welcome to the page at %v.", currentPath)), }, }) } -func Navbar(path string) g.Node { - return g.El("nav", - el.A("/", attr.Classes{"is-active": path == "/"}, g.Text("Home")), - el.A("/about", attr.Classes{"is-active": path == "/about"}, g.Text("About")), +func Navbar(currentPath string) g.Node { + return Nav( + NavbarLink("/", "Home", currentPath), + NavbarLink("/about", "About", currentPath), ) } + +func NavbarLink(href, name, currentPath string) g.Node { + return A(href, Classes{"is-active": currentPath == href}, g.Text(name)) +} ``` For more complete examples, see [the examples directory](examples/). + +### What's up with the specially named elements and attributes? + +Unfortunately, there are three 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: + +- `form` (`FormEl`/`FormAttr`) +- `style` (`StyleEl`/`StyleAttr`) +- `title` (`TitleEl`/`TitleAttr`) diff --git a/attr/boolean.go b/attr/boolean.go deleted file mode 100644 index 2a70c9d..0000000 --- a/attr/boolean.go +++ /dev/null @@ -1,45 +0,0 @@ -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/boolean_test.go b/attr/boolean_test.go deleted file mode 100644 index 309b025..0000000 --- a/attr/boolean_test.go +++ /dev/null @@ -1,32 +0,0 @@ -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></div>`, name), n) - }) - } -} diff --git a/attr/simple_test.go b/attr/simple_test.go deleted file mode 100644 index d006b76..0000000 --- a/attr/simple_test.go +++ /dev/null @@ -1,51 +0,0 @@ -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, - "charset": attr.Charset, - "class": attr.Class, - "cols": attr.Cols, - "content": attr.Content, - "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, - "rows": attr.Rows, - "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"></div>`, name), n) - }) - } -} diff --git a/attr/attributes.go b/components/attributes.go index 938d762..dc7ef9b 100644 --- a/attr/attributes.go +++ b/components/attributes.go @@ -1,6 +1,4 @@ -// Package attr provides shortcuts and helpers to common HTML attributes. -// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes for a list of attributes. -package attr +package components import ( "io" @@ -8,6 +6,7 @@ import ( "strings" g "github.com/maragudk/gomponents" + "github.com/maragudk/gomponents/html" ) // Classes is a map of strings to booleans, which Renders to an attribute with name "class". @@ -23,7 +22,7 @@ func (c Classes) Render(w io.Writer) error { } } sort.Strings(included) - return g.Attr("class", strings.Join(included, " ")).Render(w) + return html.Class(strings.Join(included, " ")).Render(w) } func (c Classes) Type() g.NodeType { diff --git a/attr/attributes_test.go b/components/attributes_test.go index da04a20..bf3e93b 100644 --- a/attr/attributes_test.go +++ b/components/attributes_test.go @@ -1,16 +1,16 @@ -package attr_test +package components_test import ( "testing" g "github.com/maragudk/gomponents" "github.com/maragudk/gomponents/assert" - "github.com/maragudk/gomponents/attr" + c "github.com/maragudk/gomponents/components" ) 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{ + assert.Equal(t, ` class="boheme-hat hat partyhat"`, c.Classes{ "boheme-hat": true, "hat": true, "partyhat": true, @@ -19,12 +19,12 @@ 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}) + e := g.El("div", c.Classes{"hat": true}) assert.Equal(t, `<div class="hat"></div>`, e) }) t.Run("also works with fmt", func(t *testing.T) { - a := attr.Classes{"hat": true} + a := c.Classes{"hat": true} if a.String() != ` class="hat"` { t.FailNow() } diff --git a/components/documents.go b/components/documents.go index 5e96466..e2d3a22 100644 --- a/components/documents.go +++ b/components/documents.go @@ -1,15 +1,14 @@ -// Package components provides high-level components that are composed of low-level elements and attributes. +// Package components provides high-level components and helpers that are composed of low-level elements and attributes. package components import ( g "github.com/maragudk/gomponents" - "github.com/maragudk/gomponents/attr" - "github.com/maragudk/gomponents/el" + . "github.com/maragudk/gomponents/html" ) -// DocumentProps for HTML5. +// HTML5Props for HTML5. // Title is set no matter what, Description and Language elements only if the strings are non-empty. -type DocumentProps struct { +type HTML5Props struct { Title string Description string Language string @@ -18,24 +17,24 @@ type DocumentProps struct { } // HTML5 document template. -func HTML5(p DocumentProps) g.NodeFunc { +func HTML5(p HTML5Props) g.NodeFunc { var lang, description g.Node if p.Language != "" { - lang = attr.Lang(p.Language) + lang = Lang(p.Language) } if p.Description != "" { - description = el.Meta(attr.Name("description"), attr.Content(p.Description)) + description = Meta(Name("description"), Content(p.Description)) } - return el.Document( - el.HTML(lang, - el.Head( - el.Meta(attr.Charset("utf-8")), - el.Meta(attr.Name("viewport"), attr.Content("width=device-width, initial-scale=1")), - el.Title(p.Title), + return Document( + HTML(lang, + Head( + Meta(Charset("utf-8")), + Meta(Name("viewport"), Content("width=device-width, initial-scale=1")), + TitleEl(p.Title), description, g.Group(p.Head), ), - el.Body(g.Group(p.Body)), + Body(g.Group(p.Body)), ), ) } diff --git a/components/documents_test.go b/components/documents_test.go index 863ac96..07f8697 100644 --- a/components/documents_test.go +++ b/components/documents_test.go @@ -5,26 +5,25 @@ import ( g "github.com/maragudk/gomponents" "github.com/maragudk/gomponents/assert" - "github.com/maragudk/gomponents/attr" - c "github.com/maragudk/gomponents/components" - "github.com/maragudk/gomponents/el" + . "github.com/maragudk/gomponents/components" + . "github.com/maragudk/gomponents/html" ) func TestHTML5(t *testing.T) { t.Run("returns an html5 document template", func(t *testing.T) { - e := c.HTML5(c.DocumentProps{ + e := HTML5(HTML5Props{ Title: "Hat", Description: "Love hats.", Language: "en", - Head: []g.Node{el.Link(attr.Rel("stylesheet"), attr.Href("/hat.css"))}, - Body: []g.Node{el.Div()}, + Head: []g.Node{Link(Rel("stylesheet"), Href("/hat.css"))}, + Body: []g.Node{Div()}, }) assert.Equal(t, `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Hat</title><meta name="description" content="Love hats."><link rel="stylesheet" href="/hat.css"></head><body><div></div></body></html>`, e) }) t.Run("returns no language, description, and extra head/body elements if empty", func(t *testing.T) { - e := c.HTML5(c.DocumentProps{ + e := HTML5(HTML5Props{ Title: "Hat", }) diff --git a/el/elements.go b/el/elements.go deleted file mode 100644 index 0841ff8..0000000 --- a/el/elements.go +++ /dev/null @@ -1,68 +0,0 @@ -// Package el provides shortcuts and helpers to common HTML elements. -// See https://developer.mozilla.org/en-US/docs/Web/HTML/Element for a list of elements. -package el - -import ( - "fmt" - "io" - - g "github.com/maragudk/gomponents" -) - -func A(href string, children ...g.Node) g.NodeFunc { - return g.El("a", g.Attr("href", href), g.Group(children)) -} - -// Document returns an special kind of Node that prefixes its child with the string "<!doctype html>". -func Document(child g.Node) g.NodeFunc { - return func(w io.Writer) error { - if _, err := w.Write([]byte("<!doctype html>")); err != nil { - return err - } - return child.Render(w) - } -} - -// 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", g.Attr("action", action), g.Attr("method", method), g.Group(children)) -} - -func Img(src, alt string, children ...g.Node) g.NodeFunc { - return g.El("img", g.Attr("src", src), g.Attr("alt", alt), 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", 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", 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", 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", - g.Attr("value", fmt.Sprintf("%v", value)), - g.Attr("max", fmt.Sprintf("%v", max)), - 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", 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", g.Attr("name", name), g.Group(children)) -} diff --git a/el/elements_test.go b/el/elements_test.go deleted file mode 100644 index adce6df..0000000 --- a/el/elements_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package el_test - -import ( - "errors" - "testing" - - g "github.com/maragudk/gomponents" - "github.com/maragudk/gomponents/assert" - "github.com/maragudk/gomponents/el" -) - -type erroringWriter struct{} - -func (w *erroringWriter) Write(p []byte) (n int, err error) { - return 0, errors.New("don't want to write") -} - -func TestDocument(t *testing.T) { - t.Run("returns doctype and children", func(t *testing.T) { - assert.Equal(t, `<!doctype html><html></html>`, el.Document(g.El("html"))) - }) - - t.Run("errors on write error in Render", func(t *testing.T) { - err := el.Document(g.El("html")).Render(&erroringWriter{}) - assert.Error(t, err) - }) -} - -func TestForm(t *testing.T) { - t.Run("returns a form element with action and method attributes", func(t *testing.T) { - assert.Equal(t, `<form action="/" method="post"></form>`, el.Form("/", "post")) - }) -} - -func TestInput(t *testing.T) { - t.Run("returns an input element with attributes type and name", func(t *testing.T) { - assert.Equal(t, `<input type="text" name="hat">`, el.Input("text", "hat")) - }) -} - -func TestLabel(t *testing.T) { - t.Run("returns a label element with attribute for", func(t *testing.T) { - assert.Equal(t, `<label for="hat">Hat</label>`, el.Label("hat", g.Text("Hat"))) - }) -} - -func TestOption(t *testing.T) { - t.Run("returns an option element with attribute label and content", func(t *testing.T) { - assert.Equal(t, `<option value="hat">Hat</option>`, el.Option("Hat", "hat")) - }) -} - -func TestProgress(t *testing.T) { - t.Run("returns a progress element with attributes value and max", func(t *testing.T) { - assert.Equal(t, `<progress value="5.5" max="10"></progress>`, el.Progress(5.5, 10)) - }) -} - -func TestSelect(t *testing.T) { - t.Run("returns a select element with attribute name", func(t *testing.T) { - assert.Equal(t, `<select name="hat"><option value="partyhat">Partyhat</option></select>`, - el.Select("hat", el.Option("Partyhat", "partyhat"))) - }) -} - -func TestTextarea(t *testing.T) { - t.Run("returns a textarea element with attribute name", func(t *testing.T) { - assert.Equal(t, `<textarea name="hat"></textarea>`, el.Textarea("hat")) - }) -} - -func TestA(t *testing.T) { - t.Run("returns an a element with a href attribute", func(t *testing.T) { - assert.Equal(t, `<a href="#">hat</a>`, el.A("#", g.Text("hat"))) - }) -} - -func TestImg(t *testing.T) { - t.Run("returns an img element with href and alt attributes", func(t *testing.T) { - assert.Equal(t, `<img src="hat.png" alt="hat" id="image">`, el.Img("hat.png", "hat", g.Attr("id", "image"))) - }) -} diff --git a/el/simple.go b/el/simple.go deleted file mode 100644 index 1ebf33f..0000000 --- a/el/simple.go +++ /dev/null @@ -1,257 +0,0 @@ -package el - -import ( - g "github.com/maragudk/gomponents" -) - -func Address(children ...g.Node) g.NodeFunc { - return g.El("address", children...) -} - -func Area(children ...g.Node) g.NodeFunc { - return g.El("area", children...) -} - -func Article(children ...g.Node) g.NodeFunc { - return g.El("article", children...) -} - -func Aside(children ...g.Node) g.NodeFunc { - return g.El("aside", children...) -} - -func Audio(children ...g.Node) g.NodeFunc { - return g.El("audio", children...) -} - -func Base(children ...g.Node) g.NodeFunc { - return g.El("base", children...) -} - -func BlockQuote(children ...g.Node) g.NodeFunc { - return g.El("blockquote", children...) -} - -func Body(children ...g.Node) g.NodeFunc { - return g.El("body", children...) -} - -func Br(children ...g.Node) g.NodeFunc { - return g.El("br", children...) -} - -func Button(children ...g.Node) g.NodeFunc { - return g.El("button", children...) -} - -func Canvas(children ...g.Node) g.NodeFunc { - return g.El("canvas", children...) -} - -func Cite(children ...g.Node) g.NodeFunc { - return g.El("cite", children...) -} - -func Code(children ...g.Node) g.NodeFunc { - return g.El("code", children...) -} - -func Col(children ...g.Node) g.NodeFunc { - return g.El("col", children...) -} - -func ColGroup(children ...g.Node) g.NodeFunc { - return g.El("colgroup", children...) -} - -func Data(children ...g.Node) g.NodeFunc { - return g.El("data", children...) -} - -func DataList(children ...g.Node) g.NodeFunc { - return g.El("datalist", children...) -} - -func Details(children ...g.Node) g.NodeFunc { - return g.El("details", children...) -} - -func Dialog(children ...g.Node) g.NodeFunc { - return g.El("dialog", children...) -} - -func Div(children ...g.Node) g.NodeFunc { - return g.El("div", children...) -} - -func Dl(children ...g.Node) g.NodeFunc { - return g.El("dl", children...) -} - -func Embed(children ...g.Node) g.NodeFunc { - return g.El("embed", children...) -} - -func FieldSet(children ...g.Node) g.NodeFunc { - return g.El("fieldset", children...) -} - -func Figure(children ...g.Node) g.NodeFunc { - return g.El("figure", children...) -} - -func Footer(children ...g.Node) g.NodeFunc { - return g.El("footer", children...) -} - -func Head(children ...g.Node) g.NodeFunc { - return g.El("head", children...) -} - -func Header(children ...g.Node) g.NodeFunc { - return g.El("header", children...) -} - -func HGroup(children ...g.Node) g.NodeFunc { - return g.El("hgroup", children...) -} - -func Hr(children ...g.Node) g.NodeFunc { - return g.El("hr", children...) -} - -func HTML(children ...g.Node) g.NodeFunc { - return g.El("html", children...) -} - -func IFrame(children ...g.Node) g.NodeFunc { - return g.El("iframe", children...) -} - -func Legend(children ...g.Node) g.NodeFunc { - return g.El("legend", children...) -} - -func Li(children ...g.Node) g.NodeFunc { - return g.El("li", children...) -} - -func Link(children ...g.Node) g.NodeFunc { - return g.El("link", children...) -} - -func Main(children ...g.Node) g.NodeFunc { - return g.El("main", children...) -} - -func Menu(children ...g.Node) g.NodeFunc { - return g.El("menu", children...) -} - -func Meta(children ...g.Node) g.NodeFunc { - return g.El("meta", children...) -} - -func Meter(children ...g.Node) g.NodeFunc { - return g.El("meter", children...) -} - -func Nav(children ...g.Node) g.NodeFunc { - return g.El("nav", children...) -} - -func NoScript(children ...g.Node) g.NodeFunc { - return g.El("noscript", children...) -} - -func Object(children ...g.Node) g.NodeFunc { - return g.El("object", children...) -} - -func Ol(children ...g.Node) g.NodeFunc { - return g.El("ol", children...) -} - -func OptGroup(children ...g.Node) g.NodeFunc { - return g.El("optgroup", children...) -} - -func P(children ...g.Node) g.NodeFunc { - return g.El("p", children...) -} - -func Param(children ...g.Node) g.NodeFunc { - return g.El("param", children...) -} - -func Picture(children ...g.Node) g.NodeFunc { - return g.El("picture", children...) -} - -func Pre(children ...g.Node) g.NodeFunc { - return g.El("pre", children...) -} - -func Script(children ...g.Node) g.NodeFunc { - return g.El("script", children...) -} - -func Section(children ...g.Node) g.NodeFunc { - return g.El("section", children...) -} - -func Source(children ...g.Node) g.NodeFunc { - return g.El("source", children...) -} - -func Span(children ...g.Node) g.NodeFunc { - return g.El("span", children...) -} - -func Style(children ...g.Node) g.NodeFunc { - return g.El("style", children...) -} - -func Summary(children ...g.Node) g.NodeFunc { - return g.El("summary", children...) -} - -func SVG(children ...g.Node) g.NodeFunc { - return g.El("svg", children...) -} - -func Table(children ...g.Node) g.NodeFunc { - return g.El("table", children...) -} - -func TBody(children ...g.Node) g.NodeFunc { - return g.El("tbody", children...) -} - -func Td(children ...g.Node) g.NodeFunc { - return g.El("td", children...) -} - -func TFoot(children ...g.Node) g.NodeFunc { - return g.El("tfoot", children...) -} - -func Th(children ...g.Node) g.NodeFunc { - return g.El("th", children...) -} - -func THead(children ...g.Node) g.NodeFunc { - return g.El("thead", children...) -} - -func Tr(children ...g.Node) g.NodeFunc { - return g.El("tr", children...) -} - -func Ul(children ...g.Node) g.NodeFunc { - return g.El("ul", children...) -} - -func Wbr(children ...g.Node) g.NodeFunc { - return g.El("wbr", children...) -} diff --git a/el/simple_test.go b/el/simple_test.go deleted file mode 100644 index 2621982..0000000 --- a/el/simple_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package el_test - -import ( - "fmt" - "testing" - - g "github.com/maragudk/gomponents" - "github.com/maragudk/gomponents/assert" - "github.com/maragudk/gomponents/el" -) - -func TestSimpleElements(t *testing.T) { - cases := map[string]func(...g.Node) g.NodeFunc{ - "address": el.Address, - "article": el.Article, - "aside": el.Aside, - "audio": el.Audio, - "blockquote": el.BlockQuote, - "body": el.Body, - "button": el.Button, - "canvas": el.Canvas, - "cite": el.Cite, - "code": el.Code, - "colgroup": el.ColGroup, - "data": el.Data, - "datalist": el.DataList, - "details": el.Details, - "dialog": el.Dialog, - "div": el.Div, - "dl": el.Dl, - "fieldset": el.FieldSet, - "figure": el.Figure, - "footer": el.Footer, - "head": el.Head, - "header": el.Header, - "hgroup": el.HGroup, - "html": el.HTML, - "iframe": el.IFrame, - "legend": el.Legend, - "li": el.Li, - "main": el.Main, - "menu": el.Menu, - "meter": el.Meter, - "nav": el.Nav, - "noscript": el.NoScript, - "object": el.Object, - "ol": el.Ol, - "optgroup": el.OptGroup, - "p": el.P, - "picture": el.Picture, - "pre": el.Pre, - "script": el.Script, - "section": el.Section, - "span": el.Span, - "style": el.Style, - "summary": el.Summary, - "svg": el.SVG, - "table": el.Table, - "tbody": el.TBody, - "td": el.Td, - "tfoot": el.TFoot, - "th": el.Th, - "thead": el.THead, - "tr": el.Tr, - "ul": el.Ul, - } - - 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) - }) - } -} - -func TestSimpleVoidKindElements(t *testing.T) { - cases := map[string]func(...g.Node) g.NodeFunc{ - "area": el.Area, - "base": el.Base, - "br": el.Br, - "col": el.Col, - "embed": el.Embed, - "hr": el.Hr, - "link": el.Link, - "meta": el.Meta, - "param": el.Param, - "source": el.Source, - "wbr": el.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) - }) - } -} diff --git a/el/text.go b/el/text.go deleted file mode 100644 index 6ab88db..0000000 --- a/el/text.go +++ /dev/null @@ -1,131 +0,0 @@ -package el - -import ( - g "github.com/maragudk/gomponents" -) - -func Abbr(text string, children ...g.Node) g.NodeFunc { - return g.El("abbr", g.Text(text), g.Group(children)) -} - -func B(text string, children ...g.Node) g.NodeFunc { - return g.El("b", g.Text(text), g.Group(children)) -} - -func Caption(text string, children ...g.Node) g.NodeFunc { - return g.El("caption", g.Text(text), g.Group(children)) -} - -func Dd(text string, children ...g.Node) g.NodeFunc { - return g.El("dd", g.Text(text), g.Group(children)) -} - -func Del(text string, children ...g.Node) g.NodeFunc { - return g.El("del", g.Text(text), g.Group(children)) -} - -func Dfn(text string, children ...g.Node) g.NodeFunc { - return g.El("dfn", g.Text(text), g.Group(children)) -} - -func Dt(text string, children ...g.Node) g.NodeFunc { - return g.El("dt", g.Text(text), g.Group(children)) -} - -func Em(text string, children ...g.Node) g.NodeFunc { - return g.El("em", g.Text(text), g.Group(children)) -} - -func FigCaption(text string, children ...g.Node) g.NodeFunc { - return g.El("figcaption", g.Text(text), g.Group(children)) -} - -// 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", 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", 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", 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", 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", 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", g.Text(text), g.Group(children)) -} - -func I(text string, children ...g.Node) g.NodeFunc { - return g.El("i", g.Text(text), g.Group(children)) -} - -func Ins(text string, children ...g.Node) g.NodeFunc { - return g.El("ins", g.Text(text), g.Group(children)) -} - -func Kbd(text string, children ...g.Node) g.NodeFunc { - return g.El("kbd", g.Text(text), g.Group(children)) -} - -func Mark(text string, children ...g.Node) g.NodeFunc { - return g.El("mark", g.Text(text), g.Group(children)) -} - -func Q(text string, children ...g.Node) g.NodeFunc { - return g.El("q", g.Text(text), g.Group(children)) -} - -func S(text string, children ...g.Node) g.NodeFunc { - return g.El("s", g.Text(text), g.Group(children)) -} - -func Samp(text string, children ...g.Node) g.NodeFunc { - return g.El("samp", g.Text(text), g.Group(children)) -} - -func Small(text string, children ...g.Node) g.NodeFunc { - return g.El("small", g.Text(text), g.Group(children)) -} - -func Strong(text string, children ...g.Node) g.NodeFunc { - return g.El("strong", g.Text(text), g.Group(children)) -} - -func Sub(text string, children ...g.Node) g.NodeFunc { - return g.El("sub", g.Text(text), g.Group(children)) -} - -func Sup(text string, children ...g.Node) g.NodeFunc { - return g.El("sup", g.Text(text), g.Group(children)) -} - -func Time(text string, children ...g.Node) g.NodeFunc { - return g.El("time", g.Text(text), g.Group(children)) -} - -func Title(title string, children ...g.Node) g.NodeFunc { - return g.El("title", g.Text(title), g.Group(children)) -} - -func U(text string, children ...g.Node) g.NodeFunc { - return g.El("u", g.Text(text), g.Group(children)) -} - -func Var(text string, children ...g.Node) g.NodeFunc { - return g.El("var", g.Text(text), g.Group(children)) -} diff --git a/el/text_test.go b/el/text_test.go deleted file mode 100644 index a751e19..0000000 --- a/el/text_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package el_test - -import ( - "fmt" - "testing" - - g "github.com/maragudk/gomponents" - "github.com/maragudk/gomponents/assert" - "github.com/maragudk/gomponents/el" -) - -func TestTextElements(t *testing.T) { - cases := map[string]func(string, ...g.Node) g.NodeFunc{ - "abbr": el.Abbr, - "b": el.B, - "caption": el.Caption, - "dd": el.Dd, - "del": el.Del, - "dfn": el.Dfn, - "dt": el.Dt, - "em": el.Em, - "figcaption": el.FigCaption, - "h1": el.H1, - "h2": el.H2, - "h3": el.H3, - "h4": el.H4, - "h5": el.H5, - "h6": el.H6, - "i": el.I, - "ins": el.Ins, - "kbd": el.Kbd, - "mark": el.Mark, - "q": el.Q, - "s": el.S, - "samp": el.Samp, - "small": el.Small, - "strong": el.Strong, - "sub": el.Sub, - "sup": el.Sup, - "time": el.Time, - "title": el.Title, - "u": el.U, - "var": el.Var, - } - - for name, fn := range cases { - t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) { - n := fn("hat", g.Attr("id", "hat")) - assert.Equal(t, fmt.Sprintf(`<%v id="hat">hat</%v>`, name, name), n) - }) - } -} diff --git a/examples/dot-import/dot-import.go b/examples/dot-import/dot-import.go index be3bd50..1d595b8 100644 --- a/examples/dot-import/dot-import.go +++ b/examples/dot-import/dot-import.go @@ -3,9 +3,9 @@ package main import ( "net/http" - . "github.com/maragudk/gomponents" + g "github.com/maragudk/gomponents" . "github.com/maragudk/gomponents/components" - . "github.com/maragudk/gomponents/el" + . "github.com/maragudk/gomponents/html" ) func main() { @@ -13,25 +13,32 @@ func main() { } func handler(w http.ResponseWriter, r *http.Request) { - p := page(props{ - title: r.URL.Path, - path: r.URL.Path, - }) - _ = p.Render(w) -} - -type props struct { - title string - path string + page := Page("Hi!", r.URL.Path) + _ = page.Render(w) } -func page(p props) Node { - return HTML5(DocumentProps{ - Title: p.title, +func Page(title, currentPath string) g.Node { + return HTML5(HTML5Props{ + Title: title, Language: "en", - Body: []Node{ - H1(p.title), - P(Textf("Welcome to the page at %v.", p.path)), + Head: []g.Node{ + StyleEl(Type("text/css"), g.Raw(".is-active{ font-weight: bold }")), + }, + Body: []g.Node{ + Navbar(currentPath), + H1(title), + P(g.Textf("Welcome to the page at %v.", currentPath)), }, }) } + +func Navbar(currentPath string) g.Node { + return Nav( + NavbarLink("/", "Home", currentPath), + NavbarLink("/about", "About", currentPath), + ) +} + +func NavbarLink(href, name, currentPath string) g.Node { + return A(href, Classes{"is-active": currentPath == href}, g.Text(name)) +} diff --git a/examples/simple/simple.go b/examples/simple/simple.go index dd6dd58..535fa6e 100644 --- a/examples/simple/simple.go +++ b/examples/simple/simple.go @@ -5,8 +5,8 @@ import ( "time" g "github.com/maragudk/gomponents" - "github.com/maragudk/gomponents/attr" - "github.com/maragudk/gomponents/el" + c "github.com/maragudk/gomponents/components" + h "github.com/maragudk/gomponents/html" ) func main() { @@ -27,22 +27,22 @@ type props struct { } func page(p props) g.Node { - return el.Document( - el.HTML(attr.Lang("en"), - el.Head( - el.Title(p.title), - el.Style(attr.Type("text/css"), + return h.Document( + h.HTML(h.Lang("en"), + h.Head( + h.TitleEl(p.title), + h.StyleEl(h.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; }"), ), ), - el.Body( + h.Body( navbar(navbarProps{path: p.path}), - el.Hr(), - el.H1(p.title), - el.P(g.Textf("Welcome to the page at %v.", p.path)), - el.P(g.Textf("Rendered at %v", time.Now())), + h.Hr(), + h.H1(p.title), + h.P(g.Textf("Welcome to the page at %v.", p.path)), + h.P(g.Textf("Rendered at %v", time.Now())), ), ), ) @@ -63,9 +63,9 @@ func navbar(props navbarProps) g.Node { } lis := g.Map(len(items), func(i int) g.Node { item := items[i] - return el.Li( - el.A(item.path, attr.Classes(map[string]bool{"is-active": props.path == item.path}), g.Text(item.text)), + return h.Li( + h.A(item.path, c.Classes(map[string]bool{"is-active": props.path == item.path}), g.Text(item.text)), ) }) - return el.Ul(attr.Class("nav"), g.Group(lis)) + return h.Ul(h.Class("nav"), g.Group(lis)) } diff --git a/attr/simple.go b/html/attributes.go index a96c139..3db8584 100644 --- a/attr/simple.go +++ b/html/attributes.go @@ -1,9 +1,49 @@ -package attr +package html 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") +} + func Accept(v string) g.Node { return g.Attr("accept", v) } @@ -28,7 +68,7 @@ func Content(v string) g.Node { return g.Attr("content", v) } -func Form(v string) g.Node { +func FormAttr(v string) g.Node { return g.Attr("form", v) } @@ -92,7 +132,7 @@ func Src(v string) g.Node { return g.Attr("src", v) } -func Style(v string) g.Node { +func StyleAttr(v string) g.Node { return g.Attr("style", v) } @@ -104,7 +144,7 @@ func Target(v string) g.Node { return g.Attr("target", v) } -func Title(v string) g.Node { +func TitleAttr(v string) g.Node { return g.Attr("title", v) } diff --git a/html/attributes_test.go b/html/attributes_test.go new file mode 100644 index 0000000..7bf45bb --- /dev/null +++ b/html/attributes_test.go @@ -0,0 +1,73 @@ +package html_test + +import ( + "fmt" + "testing" + + g "github.com/maragudk/gomponents" + "github.com/maragudk/gomponents/assert" + . "github.com/maragudk/gomponents/html" +) + +func TestBooleanAttributes(t *testing.T) { + cases := map[string]func() g.Node{ + "async": Async, + "autofocus": AutoFocus, + "autoplay": AutoPlay, + "controls": Controls, + "defer": Defer, + "disabled": Disabled, + "multiple": Multiple, + "readonly": ReadOnly, + "required": Required, + "selected": 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) + }) + } +} + +func TestSimpleAttributes(t *testing.T) { + cases := map[string]func(string) g.Node{ + "accept": Accept, + "autocomplete": AutoComplete, + "charset": Charset, + "class": Class, + "cols": Cols, + "content": Content, + "form": FormAttr, + "height": Height, + "href": Href, + "id": ID, + "lang": Lang, + "max": Max, + "maxlength": MaxLength, + "min": Min, + "minlength": MinLength, + "name": Name, + "pattern": Pattern, + "preload": Preload, + "placeholder": Placeholder, + "rel": Rel, + "rows": Rows, + "src": Src, + "style": StyleAttr, + "tabindex": TabIndex, + "target": Target, + "title": TitleAttr, + "type": Type, + "value": Value, + "width": 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) + }) + } +} diff --git a/html/elements.go b/html/elements.go new file mode 100644 index 0000000..8045561 --- /dev/null +++ b/html/elements.go @@ -0,0 +1,447 @@ +// Package html provides common HTML elements and attributes. +// See https://developer.mozilla.org/en-US/docs/Web/HTML/Element for a list of elements. +// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes for a list of attributes. +package html + +import ( + "fmt" + "io" + + g "github.com/maragudk/gomponents" +) + +func A(href string, children ...g.Node) g.NodeFunc { + return g.El("a", g.Attr("href", href), g.Group(children)) +} + +// Document returns an special kind of Node that prefixes its child with the string "<!doctype html>". +func Document(child g.Node) g.NodeFunc { + return func(w io.Writer) error { + if _, err := w.Write([]byte("<!doctype html>")); err != nil { + return err + } + return child.Render(w) + } +} + +// FormEl returns an element with name "form", the given action and method attributes, and the given children. +func FormEl(action, method string, children ...g.Node) g.NodeFunc { + return g.El("form", g.Attr("action", action), g.Attr("method", method), g.Group(children)) +} + +func Img(src, alt string, children ...g.Node) g.NodeFunc { + return g.El("img", g.Attr("src", src), g.Attr("alt", alt), 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", 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", 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", 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", + g.Attr("value", fmt.Sprintf("%v", value)), + g.Attr("max", fmt.Sprintf("%v", max)), + 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", 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", g.Attr("name", name), g.Group(children)) +} + +func Address(children ...g.Node) g.NodeFunc { + return g.El("address", children...) +} + +func Area(children ...g.Node) g.NodeFunc { + return g.El("area", children...) +} + +func Article(children ...g.Node) g.NodeFunc { + return g.El("article", children...) +} + +func Aside(children ...g.Node) g.NodeFunc { + return g.El("aside", children...) +} + +func Audio(children ...g.Node) g.NodeFunc { + return g.El("audio", children...) +} + +func Base(children ...g.Node) g.NodeFunc { + return g.El("base", children...) +} + +func BlockQuote(children ...g.Node) g.NodeFunc { + return g.El("blockquote", children...) +} + +func Body(children ...g.Node) g.NodeFunc { + return g.El("body", children...) +} + +func Br(children ...g.Node) g.NodeFunc { + return g.El("br", children...) +} + +func Button(children ...g.Node) g.NodeFunc { + return g.El("button", children...) +} + +func Canvas(children ...g.Node) g.NodeFunc { + return g.El("canvas", children...) +} + +func Cite(children ...g.Node) g.NodeFunc { + return g.El("cite", children...) +} + +func Code(children ...g.Node) g.NodeFunc { + return g.El("code", children...) +} + +func Col(children ...g.Node) g.NodeFunc { + return g.El("col", children...) +} + +func ColGroup(children ...g.Node) g.NodeFunc { + return g.El("colgroup", children...) +} + +func Data(children ...g.Node) g.NodeFunc { + return g.El("data", children...) +} + +func DataList(children ...g.Node) g.NodeFunc { + return g.El("datalist", children...) +} + +func Details(children ...g.Node) g.NodeFunc { + return g.El("details", children...) +} + +func Dialog(children ...g.Node) g.NodeFunc { + return g.El("dialog", children...) +} + +func Div(children ...g.Node) g.NodeFunc { + return g.El("div", children...) +} + +func Dl(children ...g.Node) g.NodeFunc { + return g.El("dl", children...) +} + +func Embed(children ...g.Node) g.NodeFunc { + return g.El("embed", children...) +} + +func FieldSet(children ...g.Node) g.NodeFunc { + return g.El("fieldset", children...) +} + +func Figure(children ...g.Node) g.NodeFunc { + return g.El("figure", children...) +} + +func Footer(children ...g.Node) g.NodeFunc { + return g.El("footer", children...) +} + +func Head(children ...g.Node) g.NodeFunc { + return g.El("head", children...) +} + +func Header(children ...g.Node) g.NodeFunc { + return g.El("header", children...) +} + +func HGroup(children ...g.Node) g.NodeFunc { + return g.El("hgroup", children...) +} + +func Hr(children ...g.Node) g.NodeFunc { + return g.El("hr", children...) +} + +func HTML(children ...g.Node) g.NodeFunc { + return g.El("html", children...) +} + +func IFrame(children ...g.Node) g.NodeFunc { + return g.El("iframe", children...) +} + +func Legend(children ...g.Node) g.NodeFunc { + return g.El("legend", children...) +} + +func Li(children ...g.Node) g.NodeFunc { + return g.El("li", children...) +} + +func Link(children ...g.Node) g.NodeFunc { + return g.El("link", children...) +} + +func Main(children ...g.Node) g.NodeFunc { + return g.El("main", children...) +} + +func Menu(children ...g.Node) g.NodeFunc { + return g.El("menu", children...) +} + +func Meta(children ...g.Node) g.NodeFunc { + return g.El("meta", children...) +} + +func Meter(children ...g.Node) g.NodeFunc { + return g.El("meter", children...) +} + +func Nav(children ...g.Node) g.NodeFunc { + return g.El("nav", children...) +} + +func NoScript(children ...g.Node) g.NodeFunc { + return g.El("noscript", children...) +} + +func Object(children ...g.Node) g.NodeFunc { + return g.El("object", children...) +} + +func Ol(children ...g.Node) g.NodeFunc { + return g.El("ol", children...) +} + +func OptGroup(children ...g.Node) g.NodeFunc { + return g.El("optgroup", children...) +} + +func P(children ...g.Node) g.NodeFunc { + return g.El("p", children...) +} + +func Param(children ...g.Node) g.NodeFunc { + return g.El("param", children...) +} + +func Picture(children ...g.Node) g.NodeFunc { + return g.El("picture", children...) +} + +func Pre(children ...g.Node) g.NodeFunc { + return g.El("pre", children...) +} + +func Script(children ...g.Node) g.NodeFunc { + return g.El("script", children...) +} + +func Section(children ...g.Node) g.NodeFunc { + return g.El("section", children...) +} + +func Source(children ...g.Node) g.NodeFunc { + return g.El("source", children...) +} + +func Span(children ...g.Node) g.NodeFunc { + return g.El("span", children...) +} + +func StyleEl(children ...g.Node) g.NodeFunc { + return g.El("style", children...) +} + +func Summary(children ...g.Node) g.NodeFunc { + return g.El("summary", children...) +} + +func SVG(children ...g.Node) g.NodeFunc { + return g.El("svg", children...) +} + +func Table(children ...g.Node) g.NodeFunc { + return g.El("table", children...) +} + +func TBody(children ...g.Node) g.NodeFunc { + return g.El("tbody", children...) +} + +func Td(children ...g.Node) g.NodeFunc { + return g.El("td", children...) +} + +func TFoot(children ...g.Node) g.NodeFunc { + return g.El("tfoot", children...) +} + +func Th(children ...g.Node) g.NodeFunc { + return g.El("th", children...) +} + +func THead(children ...g.Node) g.NodeFunc { + return g.El("thead", children...) +} + +func Tr(children ...g.Node) g.NodeFunc { + return g.El("tr", children...) +} + +func Ul(children ...g.Node) g.NodeFunc { + return g.El("ul", children...) +} + +func Wbr(children ...g.Node) g.NodeFunc { + return g.El("wbr", children...) +} + +func Abbr(text string, children ...g.Node) g.NodeFunc { + return g.El("abbr", g.Text(text), g.Group(children)) +} + +func B(text string, children ...g.Node) g.NodeFunc { + return g.El("b", g.Text(text), g.Group(children)) +} + +func Caption(text string, children ...g.Node) g.NodeFunc { + return g.El("caption", g.Text(text), g.Group(children)) +} + +func Dd(text string, children ...g.Node) g.NodeFunc { + return g.El("dd", g.Text(text), g.Group(children)) +} + +func Del(text string, children ...g.Node) g.NodeFunc { + return g.El("del", g.Text(text), g.Group(children)) +} + +func Dfn(text string, children ...g.Node) g.NodeFunc { + return g.El("dfn", g.Text(text), g.Group(children)) +} + +func Dt(text string, children ...g.Node) g.NodeFunc { + return g.El("dt", g.Text(text), g.Group(children)) +} + +func Em(text string, children ...g.Node) g.NodeFunc { + return g.El("em", g.Text(text), g.Group(children)) +} + +func FigCaption(text string, children ...g.Node) g.NodeFunc { + return g.El("figcaption", g.Text(text), g.Group(children)) +} + +// 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", 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", 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", 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", 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", 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", g.Text(text), g.Group(children)) +} + +func I(text string, children ...g.Node) g.NodeFunc { + return g.El("i", g.Text(text), g.Group(children)) +} + +func Ins(text string, children ...g.Node) g.NodeFunc { + return g.El("ins", g.Text(text), g.Group(children)) +} + +func Kbd(text string, children ...g.Node) g.NodeFunc { + return g.El("kbd", g.Text(text), g.Group(children)) +} + +func Mark(text string, children ...g.Node) g.NodeFunc { + return g.El("mark", g.Text(text), g.Group(children)) +} + +func Q(text string, children ...g.Node) g.NodeFunc { + return g.El("q", g.Text(text), g.Group(children)) +} + +func S(text string, children ...g.Node) g.NodeFunc { + return g.El("s", g.Text(text), g.Group(children)) +} + +func Samp(text string, children ...g.Node) g.NodeFunc { + return g.El("samp", g.Text(text), g.Group(children)) +} + +func Small(text string, children ...g.Node) g.NodeFunc { + return g.El("small", g.Text(text), g.Group(children)) +} + +func Strong(text string, children ...g.Node) g.NodeFunc { + return g.El("strong", g.Text(text), g.Group(children)) +} + +func Sub(text string, children ...g.Node) g.NodeFunc { + return g.El("sub", g.Text(text), g.Group(children)) +} + +func Sup(text string, children ...g.Node) g.NodeFunc { + return g.El("sup", g.Text(text), g.Group(children)) +} + +func Time(text string, children ...g.Node) g.NodeFunc { + return g.El("time", g.Text(text), g.Group(children)) +} + +func TitleEl(title string, children ...g.Node) g.NodeFunc { + return g.El("title", g.Text(title), g.Group(children)) +} + +func U(text string, children ...g.Node) g.NodeFunc { + return g.El("u", g.Text(text), g.Group(children)) +} + +func Var(text string, children ...g.Node) g.NodeFunc { + return g.El("var", g.Text(text), g.Group(children)) +} diff --git a/html/elements_test.go b/html/elements_test.go new file mode 100644 index 0000000..e550bfe --- /dev/null +++ b/html/elements_test.go @@ -0,0 +1,212 @@ +package html_test + +import ( + "errors" + "fmt" + "testing" + + g "github.com/maragudk/gomponents" + "github.com/maragudk/gomponents/assert" + . "github.com/maragudk/gomponents/html" +) + +type erroringWriter struct{} + +func (w *erroringWriter) Write(p []byte) (n int, err error) { + return 0, errors.New("don't want to write") +} + +func TestDocument(t *testing.T) { + t.Run("returns doctype and children", func(t *testing.T) { + assert.Equal(t, `<!doctype html><html></html>`, Document(g.El("html"))) + }) + + t.Run("errors on write error in Render", func(t *testing.T) { + err := Document(g.El("html")).Render(&erroringWriter{}) + assert.Error(t, err) + }) +} + +func TestFormEl(t *testing.T) { + t.Run("returns a form element with action and method attributes", func(t *testing.T) { + assert.Equal(t, `<form action="/" method="post"></form>`, FormEl("/", "post")) + }) +} + +func TestInput(t *testing.T) { + t.Run("returns an input element with attributes type and name", func(t *testing.T) { + assert.Equal(t, `<input type="text" name="hat">`, Input("text", "hat")) + }) +} + +func TestLabel(t *testing.T) { + t.Run("returns a label element with attribute for", func(t *testing.T) { + assert.Equal(t, `<label for="hat">Hat</label>`, Label("hat", g.Text("Hat"))) + }) +} + +func TestOption(t *testing.T) { + t.Run("returns an option element with attribute label and content", func(t *testing.T) { + assert.Equal(t, `<option value="hat">Hat</option>`, Option("Hat", "hat")) + }) +} + +func TestProgress(t *testing.T) { + t.Run("returns a progress element with attributes value and max", func(t *testing.T) { + assert.Equal(t, `<progress value="5.5" max="10"></progress>`, Progress(5.5, 10)) + }) +} + +func TestSelect(t *testing.T) { + t.Run("returns a select element with attribute name", func(t *testing.T) { + assert.Equal(t, `<select name="hat"><option value="partyhat">Partyhat</option></select>`, + Select("hat", Option("Partyhat", "partyhat"))) + }) +} + +func TestTextarea(t *testing.T) { + t.Run("returns a textarea element with attribute name", func(t *testing.T) { + assert.Equal(t, `<textarea name="hat"></textarea>`, Textarea("hat")) + }) +} + +func TestA(t *testing.T) { + t.Run("returns an a element with a href attribute", func(t *testing.T) { + assert.Equal(t, `<a href="#">hat</a>`, A("#", g.Text("hat"))) + }) +} + +func TestImg(t *testing.T) { + t.Run("returns an img element with href and alt attributes", func(t *testing.T) { + assert.Equal(t, `<img src="hat.png" alt="hat" id="image">`, Img("hat.png", "hat", g.Attr("id", "image"))) + }) +} + +func TestSimpleElements(t *testing.T) { + cases := map[string]func(...g.Node) g.NodeFunc{ + "address": Address, + "article": Article, + "aside": Aside, + "audio": Audio, + "blockquote": BlockQuote, + "body": Body, + "button": Button, + "canvas": Canvas, + "cite": Cite, + "code": Code, + "colgroup": ColGroup, + "data": Data, + "datalist": DataList, + "details": Details, + "dialog": Dialog, + "div": Div, + "dl": Dl, + "fieldset": FieldSet, + "figure": Figure, + "footer": Footer, + "head": Head, + "header": Header, + "hgroup": HGroup, + "html": HTML, + "iframe": IFrame, + "legend": Legend, + "li": Li, + "main": Main, + "menu": Menu, + "meter": Meter, + "nav": Nav, + "noscript": NoScript, + "object": Object, + "ol": Ol, + "optgroup": OptGroup, + "p": P, + "picture": Picture, + "pre": Pre, + "script": Script, + "section": Section, + "span": Span, + "style": StyleEl, + "summary": Summary, + "svg": SVG, + "table": Table, + "tbody": TBody, + "td": Td, + "tfoot": TFoot, + "th": Th, + "thead": THead, + "tr": Tr, + "ul": Ul, + } + + 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) + }) + } +} + +func TestSimpleVoidKindElements(t *testing.T) { + cases := map[string]func(...g.Node) g.NodeFunc{ + "area": Area, + "base": Base, + "br": Br, + "col": Col, + "embed": Embed, + "hr": Hr, + "link": Link, + "meta": Meta, + "param": Param, + "source": Source, + "wbr": 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) + }) + } +} + +func TestTextElements(t *testing.T) { + cases := map[string]func(string, ...g.Node) g.NodeFunc{ + "abbr": Abbr, + "b": B, + "caption": Caption, + "dd": Dd, + "del": Del, + "dfn": Dfn, + "dt": Dt, + "em": Em, + "figcaption": FigCaption, + "h1": H1, + "h2": H2, + "h3": H3, + "h4": H4, + "h5": H5, + "h6": H6, + "i": I, + "ins": Ins, + "kbd": Kbd, + "mark": Mark, + "q": Q, + "s": S, + "samp": Samp, + "small": Small, + "strong": Strong, + "sub": Sub, + "sup": Sup, + "time": Time, + "title": TitleEl, + "u": U, + "var": Var, + } + + for name, fn := range cases { + t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) { + n := fn("hat", g.Attr("id", "hat")) + assert.Equal(t, fmt.Sprintf(`<%v id="hat">hat</%v>`, name, name), n) + }) + } +} |