Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Fragment #181

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 52 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 group like this is still important even though a group can now render itself,
// since otherwise attributes would be ignored at the first level.
if g, ok := c.(group); ok {
for _, groupC := range g.children {
renderChild(w, groupC, t)
Expand Down Expand Up @@ -241,21 +252,46 @@ 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}
}

type fragment struct {
children []Node
}

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

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

// Fragment is multiple nodes concatenated. It's a lot like [Group], except it's variadic.
// It's useful in situations where you want to return an HTML fragment without a parent element.
// Note that if any of the direct children are [AttributeType], they will be ignored, to not produce invalid HTML.
func Fragment(children ...Node) Node {
return fragment{children}
}

// If condition is true, return the given [Node]. Otherwise, return nil.
// This helper function is good for inlining elements conditionally.
// If it's important that the given [Node] is only evaluated if condition is true
Expand Down
77 changes: 57 additions & 20 deletions gomponents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,35 +234,72 @@ 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.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("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()
}
})
}

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 {
func TestFragment(t *testing.T) {
t.Run("groups multiple nodes into one", func(t *testing.T) {
n := g.Fragment(g.El("div"), g.El("span"))
assert.Equal(t, "<div></div><span></span>", n)
})

t.Run("ignores attributes at the first level", func(t *testing.T) {
n := g.Fragment(g.Attr("class", "hat"), g.El("div"), g.El("span"))
assert.Equal(t, "<div></div><span></span>", n)
})

t.Run("does not ignore attributes at the second level", func(t *testing.T) {
n := g.Fragment(g.El("div", g.Attr("class", "hat")), g.El("span"))
assert.Equal(t, `<div class="hat"></div><span></span>`, n)
})

t.Run("can render a fragment child node", func(t *testing.T) {
n := g.El("div", g.Fragment(g.El("div"), g.El("span")))
assert.Equal(t, "<div><div></div><span></span></div>", n)
})

t.Run("implements fmt.Stringer", func(t *testing.T) {
n := g.Fragment(g.El("div"), g.El("span"))
if n, ok := n.(fmt.Stringer); !ok || n.String() != "<div></div><span></span>" {
t.FailNow()
}
})
}

func ExampleFragment() {
f := g.Fragment(
g.El("div"),
g.El("span"),
)

_ = f.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
Loading