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 Lazy function #156

Closed
wants to merge 4 commits into from
Closed
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
38 changes: 38 additions & 0 deletions gomponents.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two thoughts:

  1. This seems to be complicated by implementing the optional nodeTypeDescriber interface, but I've been reading through the code and it seems like the only use of nodeTypeDescriber is to omit the output if the type is wrong here
    if p, ok := c.(nodeTypeDescriber); !ok || p.Type() == ElementType {
    so maybe Lazy can just not implement it without much being lost
  2. If NodeType is important, and there are only two node types, maybe create a LazyAttr function and a LazyEl function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vikstrous2 thank you for the review! Sorry for the delay in replying, the winter and its various sicknesses has not been kind to me and my family this year… 🤒

Yes, nodeTypeDescriber is only used for checking whether a node should be rendered as an element or attribute. I think it's pretty useful in e.g. form checkboxes and the checked attribute, and it feels wrong to leave out support for attributes, even though it would simplify the API quite a bit here.

LazyAttr would also solve this, arguably more elegantly, but I would really like there to not be two functions for this already slightly edge-case functionality.

Hmm. I'm seriously considering dropping this whole Lazy stuff, it doesn't quite feel right.

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
}
61 changes: 61 additions & 0 deletions gomponents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,64 @@ func ExampleIf() {
_ = e.Render(os.Stdout)
// Output: <div><span>You lost your hat!</span></div>
}

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, "<div></div>", 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, "<div><span></span></div>", 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, "<input disabled>", 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: <div><span>Anonymous</span></div>
}
Loading