Skip to content

Commit

Permalink
Make Groups renderable (#181)
Browse files Browse the repository at this point in the history
This change makes the result of `Group` renderable directly, instead of
panicking, with the important caveat that root-level attributes are
_ignored_. I don't think this will give problems in practice, as the
main use case for rendering `Group` is basically to return root-level
elements to the client using something like HTMX.

I tried adding a `Fragment`, but it was weird and confusing having two
functions (`Group` and `Fragment`) do essentially the same thing, the
only difference being whether the argument was a slice of `Node`s or
varargs.

Fixes #162
  • Loading branch information
markuswustenberg authored Sep 19, 2024
1 parent 9c29bfc commit c97605a
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 38 deletions.
45 changes: 29 additions & 16 deletions gomponents.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,37 @@ func (n NodeFunc) String() string {
// If an element is a void element, non-attribute children nodes are ignored.
// Use this if no convenience creator exists in the html package.
func El(name string, children ...Node) Node {
return NodeFunc(func(w2 io.Writer) error {
w := &statefulWriter{w: w2}
return NodeFunc(func(w io.Writer) error {
return render(w, &name, children...)
})
}

func render(w2 io.Writer, name *string, children ...Node) error {
w := &statefulWriter{w: w2}

w.Write([]byte("<" + name))
if name != nil {
w.Write([]byte("<" + *name))

for _, c := range children {
renderChild(w, c, AttributeType)
}

w.Write([]byte(">"))

if isVoidElement(name) {
if isVoidElement(*name) {
return w.err
}
}

for _, c := range children {
renderChild(w, c, ElementType)
}
for _, c := range children {
renderChild(w, c, ElementType)
}

w.Write([]byte("</" + name + ">"))
return w.err
})
if name != nil {
w.Write([]byte("</" + *name + ">"))
}

return w.err
}

// renderChild c to the given writer w if the node type is t.
Expand All @@ -102,6 +111,8 @@ func renderChild(w *statefulWriter, c Node, t NodeType) {
return
}

// 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 {
renderChild(w, groupC, t)
Expand Down Expand Up @@ -241,17 +252,19 @@ type group struct {

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

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

// Group multiple Nodes into one Node. Useful for concatenation of Nodes in variadic functions.
// The resulting Node cannot Render directly, trying it will panic.
// Render must happen through a parent element created with El or a helper.
// 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}
}
Expand Down
49 changes: 27 additions & 22 deletions gomponents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,35 +234,40 @@ func TestGroup(t *testing.T) {
assert.Equal(t, `<div class="foo"><img><br id="hat"><hr></div>`, e)
})

t.Run("panics on direct render", func(t *testing.T) {
e := g.Group(nil)
panicked := false
defer func() {
if err := recover(); err != nil {
panicked = true
}
}()
_ = e.Render(nil)
if !panicked {
t.FailNow()
}
t.Run("ignores attributes at the first level", func(t *testing.T) {
children := []g.Node{g.Attr("class", "hat"), g.El("div"), g.El("span")}
e := g.Group(children)
assert.Equal(t, "<div></div><span></span>", e)
})

t.Run("panics on direct string", func(t *testing.T) {
e := g.Group(nil).(fmt.Stringer)
panicked := false
defer func() {
if err := recover(); err != nil {
panicked = true
}
}()
_ = e.String()
if !panicked {
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")}
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)
})

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>" {
t.FailNow()
}
})
}

func ExampleGroup() {
children := []g.Node{g.El("div"), g.El("span")}
e := g.Group(children)
_ = 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 c97605a

Please sign in to comment.