diff --git a/gomponents.go b/gomponents.go index 7b73a3b..cb59964 100644 --- a/gomponents.go +++ b/gomponents.go @@ -254,9 +254,47 @@ func Group(children []Node) Node { // If condition is true, return the given Node. Otherwise, return nil. // This helper function is good for inlining elements conditionally. +// This doesn't behave entirely like a regular if statement, because nested Nodes and their parameters are evaluated +// from the deepest level and up before hitting If (because that's how Go works). +// If you need lazy evaluation of the given Node, see Lazy. func If(condition bool, n Node) Node { if condition { return n } return nil } + +// Lazy returns a Node which calls the given callback function only when rendered. +// Used with If, it enables lazy evaluation of the node to return, depending on the condition passed to If. +// It defaults to being an ElementType, but can be set to AttributeType by passing a NodeType. +// Even though nodeType is variadic, only the first NodeType is used. +func Lazy(cb func() Node, nodeType ...NodeType) Node { + if cb == nil { + panic("lazy callback cannot be nil") + } + + switch len(nodeType) { + case 0: + return &lazy{CB: cb} + case 1: + return &lazy{CB: cb, T: nodeType[0]} + default: + panic("lazy only accepts one node type") + } +} + +// Lazy is returned by Lazy. +type lazy struct { + CB func() Node + T NodeType +} + +// Render satisfies Node. +func (l *lazy) Render(w io.Writer) error { + return l.CB().Render(w) +} + +// Type satisfies nodeTypeDescriber. +func (l *lazy) Type() NodeType { + return l.T +} diff --git a/gomponents_test.go b/gomponents_test.go index 32304f3..c0b22de 100644 --- a/gomponents_test.go +++ b/gomponents_test.go @@ -284,3 +284,64 @@ func ExampleIf() { _ = e.Render(os.Stdout) // Output:
You lost your hat!
} + +func TestLazy(t *testing.T) { + t.Run("panics when callback is nil", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.FailNow() + } + }() + g.Lazy(nil) + }) + + t.Run("calls callback on render", func(t *testing.T) { + n := g.Lazy(func() g.Node { + return g.El("div") + }) + assert.Equal(t, "
", n) + }) + + t.Run("can render elements with node type passed", func(t *testing.T) { + n := g.El("div", g.Lazy(func() g.Node { + return g.El("span") + }, g.ElementType)) + assert.Equal(t, "
", n) + }) + + t.Run("can render attributes", func(t *testing.T) { + n := g.El("input", g.Lazy(func() g.Node { + return g.Attr("disabled") + }, g.AttributeType)) + assert.Equal(t, "", n) + }) + + t.Run("panics when passed multiple node types", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.FailNow() + } + }() + g.Lazy(func() g.Node { + return nil + }, g.ElementType, g.AttributeType) + }) +} + +func ExampleIf_lazy() { + type User struct { + Name string + } + + var u *User + + e := g.El("div", + // This would panic without the Lazy call. + g.If(u != nil, g.Lazy(func() g.Node { + return g.El("span", g.Text(u.Name)) + })), + g.If(u == nil, g.El("span", g.Text("Anonymous"))), + ) + _ = e.Render(os.Stdout) + // Output:
Anonymous
+}