Skip to content

Commit

Permalink
Make Group a type (#202)
Browse files Browse the repository at this point in the history
This changes `Group` to be a type instead of a function, which means it
can support both grouping `[]Node` as well as doing variadic-ish slice things
like `Group{n1, n2}`.

This also means that `Map` can just return a `Group`.

Special thanks to @deitrix for this simple and elegant solution that,
for some reason or another, has had me perplexed for a long time.

Fixes #201.
  • Loading branch information
markuswustenberg authored Sep 24, 2024
1 parent 3a3de29 commit f58a066
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 30 deletions.
28 changes: 11 additions & 17 deletions gomponents.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ func renderChild(w *statefulWriter, c Node, t NodeType) {

// Rendering groups like this is still important even though a group can render itself,
// since otherwise attributes will sometimes be ignored.
if g, ok := c.(group); ok {
for _, groupC := range g.children {
if g, ok := c.(Group); ok {
for _, groupC := range g {
renderChild(w, groupC, t)
}
return
Expand Down Expand Up @@ -246,36 +246,30 @@ func Rawf(format string, a ...interface{}) Node {
})
}

// Map a slice of anything to a slice of Nodes.
func Map[T any](ts []T, cb func(T) Node) []Node {
// Map a slice of anything to a [Group] (which is just a slice of [Node]s).
func Map[T any](ts []T, cb func(T) Node) Group {
var nodes []Node
for _, t := range ts {
nodes = append(nodes, cb(t))
}
return nodes
}

type group struct {
children []Node
}
// Group a slice of [Node]s into one Node, while still being usable like a regular slice of [Node]s.
// A [Group] can render directly, but if any of the direct children are [AttributeType], they will be ignored,
// to not produce invalid HTML.
type Group []Node

// String satisfies [fmt.Stringer].
func (g group) String() string {
func (g Group) String() string {
var b strings.Builder
_ = g.Render(&b)
return b.String()
}

// Render satisfies [Node].
func (g group) Render(w io.Writer) error {
return render(w, nil, g.children...)
}

// Group a slice of Nodes into one Node. Useful for grouping the result of [Map] into one [Node].
// A [Group] can render directly, but if any of the direct children are [AttributeType], they will be ignored,
// to not produce invalid HTML.
func Group(children []Node) Node {
return group{children: children}
func (g Group) Render(w io.Writer) error {
return render(w, nil, g...)
}

// If condition is true, return the given [Node]. Otherwise, return nil.
Expand Down
39 changes: 26 additions & 13 deletions gomponents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func ExampleRawf() {
}

func TestMap(t *testing.T) {
t.Run("maps slices to nodes", func(t *testing.T) {
t.Run("maps a slice to a group", func(t *testing.T) {
items := []string{"hat", "partyhat", "turtlehat"}
lis := g.Map(items, func(i string) g.Node {
return g.El("li", g.Text(i))
Expand All @@ -237,14 +237,20 @@ func TestMap(t *testing.T) {
list := g.El("ul", lis...)

assert.Equal(t, `<ul><li>hat</li><li>partyhat</li><li>turtlehat</li></ul>`, list)
if len(lis) != 3 {
t.FailNow()
}
assert.Equal(t, `<li>hat</li>`, lis[0])
assert.Equal(t, `<li>partyhat</li>`, lis[1])
assert.Equal(t, `<li>turtlehat</li>`, lis[2])
})
}

func ExampleMap() {
items := []string{"party hat", "super hat"}
e := g.El("ul", g.Group(g.Map(items, func(i string) g.Node {
e := g.El("ul", g.Map(items, func(i string) g.Node {
return g.El("li", g.Text(i))
})))
}))
_ = e.Render(os.Stdout)
// Output: <ul><li>party hat</li><li>super hat</li></ul>
}
Expand All @@ -262,25 +268,26 @@ func TestGroup(t *testing.T) {
assert.Equal(t, "<div></div><span></span>", e)
})

t.Run("does not ignore attributes at the second level", func(t *testing.T) {
children := []g.Node{g.El("div", g.Attr("class", "hat")), g.El("span")}
t.Run("does not ignore attributes at the second level and below", func(t *testing.T) {
children := []g.Node{g.El("div", g.Attr("class", "hat"), g.El("hr", g.Attr("id", "partyhat"))), g.El("span")}
e := g.Group(children)
assert.Equal(t, `<div class="hat"></div><span></span>`, e)
})

t.Run("can render a group child node including attributes", func(t *testing.T) {
children := []g.Node{g.Attr("id", "hat"), g.El("div"), g.El("span")}
e := g.El("div", g.Group(children))
assert.Equal(t, `<div id="hat"><div></div><span></span></div>`, e)
assert.Equal(t, `<div class="hat"><hr id="partyhat"></div><span></span>`, e)
})

t.Run("implements fmt.Stringer", func(t *testing.T) {
children := []g.Node{g.El("div"), g.El("span")}
e := g.Group(children)
if e, ok := e.(fmt.Stringer); !ok || e.String() != "<div></div><span></span>" {
if e, ok := any(e).(fmt.Stringer); !ok || e.String() != "<div></div><span></span>" {
t.FailNow()
}
})

t.Run("can be used like a regular slice", func(t *testing.T) {
e := g.Group{g.El("div"), g.El("span")}
assert.Equal(t, "<div></div><span></span>", e)
assert.Equal(t, "<div></div>", e[0])
assert.Equal(t, "<span></span>", e[1])
})
}

func ExampleGroup() {
Expand All @@ -290,6 +297,12 @@ func ExampleGroup() {
// Output: <div></div><span></span>
}

func ExampleGroup_slice() {
e := g.Group{g.El("div"), g.El("span")}
_ = e.Render(os.Stdout)
// Output: <div></div><span></span>
}

func TestIf(t *testing.T) {
t.Run("returns node if condition is true", func(t *testing.T) {
n := g.El("div", g.If(true, g.El("span")))
Expand Down

0 comments on commit f58a066

Please sign in to comment.