Repository: maragudk/gomponents
Pull request #172 (2024-06-05 20:20:46)

Add a `Iff` helper
I ran into some situation where I want to conditionally render a node if some variable is not nil and obviously I got a panic

```go
// package viewmodels
type SomePage struct {
	s *string
}

// package views
func SomePage (vm viewmodels.SomePage) g,Node {
	return Div(
		If(vm.s == nil, Text("s is nil"),
		If(vm.s !- nil, Text("s is " + vm.s), // this will panic when `s` is nil
	)
}
```

In this situation, go will interpret the code of the second `if` regardless of the condition because the code itself is not in a condition.

This PR introduces a new `Iff` helper that accepts a callback. The callback content is only interpreted when it's called, making the code safe:

```go
// package viewmodels
type SomePage struct {
	s *string
}

// package views
func SomePage (vm viewmodels.SomePage) g,Node {
	return Div(
		Iff(vm.s == nil, func () g.Node { return Text("s is nil") },
		Iff(vm.s !- nil, func () g.Node { return Text("s is " + vm.s) },
	)
}
```

I'm aware of the `Lazy` effort on the side, but I guess this is no a breaking change and can still exist in addition to the `Lazy` effort.

markuswustenberg: Hi @JulienTant , thank you for the PR! I've done something very similar in #99, and actually attempted to resolve this in #101 and #156 as well. I've never reached a solution I really like, essentially because I think it's confusing to have two different conditional functions from a user perspective.

I really would have liked the foresight to just implement `If` this way, because you're definitely not the first to hit this… I don't know what to call it, "curiosity". But maybe this very straightforward solution is just the simplest way.

Between your solution here and the `Lazy` approach, which would you prefer?
JulienTant: I think I like having two separate options because not all the `If`s need a callback, amd callback just ain't pretty in go. It's not like we can have 

```ts
If(things == nil, () => P(g.Text("No things"))
```

Looking at the stdlib's [`slices`](https://pkg.go.dev/slices) for example, most functions also have a `*Func` equivalent so this is not shocking.

As an alternative, we could promote a new callback type and it works natively with `If`: 

```go
type NodeCallback func() g.Node

func (f NodeCallback) Render(w io.Writer) error {
    return f().Render(w)
}

func TestIfWithNodeFunc(t *testing.T) {
    t.Run("return the nil case without panicking", func(t *testing.T) {
        var nilable *string
        n := g.El("div",
            g.If(nilable != nil, NodeCallback(func() g.Node {
                return g.El("span", g.Text(*nilable))
            })),
            g.If(nilable == nil, NodeCallback(func() g.Node {
                return g.El("span", g.Text("No messages."))
            })),
        )
        assert.Equal(t, "<div><span>No messages.</span></div>", n)
    })

    t.Run("returns the non nil case", func(t *testing.T) {
        var notNil *string = new(string)
        *notNil = "You lost your hat!"
        n := g.El("div",
            g.If(notNil != nil, NodeCallback(func() g.Node {
                return g.El("span", g.Text(*notNil))
            })),
            g.If(notNil == nil, NodeCallback(func() g.Node {
                return g.El("span", g.Text("No messages."))
            })),
        )
        assert.Equal(t, "<div><span>You lost your hat!</span></div>", n)
    })
}
```
markuswustenberg: @JulienTant I've been thinking about this.

Your `NodeCallback` is essentially the `Lazy` type I've been implementing in #156, and although it's fine, it's a bit awkward to make it support both elements and attributes (like I did with `Lazy`).

What if we instead adopted a pattern of a function that is immediately evaluated? Something like this: https://go.dev/play/p/mlByJAc-Upp

```go
package main

import (
	"os"

	g "github.com/maragudk/gomponents"
)

func main() {
	_ = g.If(true, func() g.Node {
		return g.El("div")
	}()).Render(os.Stdout)
}
```

I'm not sure how to make that work with the nil pointer panics without having another `if` statement in the function though…

Otherwise, I'm leaning towards adding your `Iff`. I think it's simple and elegant.
markuswustenberg: Sidenote: I like the conciseness of `Iff` (and the reference to math's if-and-only-if, right?), but the consistency and obviousness of `IfFunc`. Which would you prefer?
JulienTant: As you mentioned, having a self-executing function here would result in the same problem when trying to deal with `nil`'s

```go
var n *string
_ = g.If(n != nil, func() g.Node {
	return g.Text(n)  // will panic
}()).Render(os.Stdout)
```

---

I prefer `Iff` and its `if and only if` association, mostly because `IfFunc` makes me believe that the `if` gets the `func`:

```go
func IfFunc(f func() bool, n g.Node) g.Node {
	if (f()) {
		return n
	}
	return nil
})
```

I think the big advantage of this solution is that it does not require any overhead for devs compared to the Lazy approach where you have to specify if it's an element or an attribute
markuswustenberg: @JulienTant I think I'm convinced about going with `Iff`. Thank you so much for discussing this with me, it's been a "gotcha" pretty much since the project started gaining traction. :D

I'll merge this and update some docs and readmes, and then release it sometime soon.

Patch:
diff --git a/gomponents.go b/gomponents.go
index 7b73a3b..ae7cadc 100644
--- a/gomponents.go
+++ b/gomponents.go
@@ -254,9 +254,20 @@ 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.
+// If your condition and node involve a nilable variable, use iff because
+// go will evaluate the node regardless of the condition.
 func If(condition bool, n Node) Node {
 	if condition {
 		return n
 	}
 	return nil
 }
+
+// Iff execute the function f if condition is true, otherwise return nil.
+// it is the preferred way to conditionally render a node if the node involves a nilable variable.
+func Iff(condition bool, f func() Node) Node {
+	if condition {
+		return f()
+	}
+	return nil
+}


Test patch:
diff --git a/gomponents_test.go b/gomponents_test.go
index 32304f3..df5da4f 100644
--- a/gomponents_test.go
+++ b/gomponents_test.go
@@ -284,3 +284,32 @@ func ExampleIf() {
 	_ = e.Render(os.Stdout)
 	// Output: <div><span>You lost your hat!</span></div>
 }
+
+func TestIff(t *testing.T) {
+	t.Run("returns node if condition is true", func(t *testing.T) {
+		n := g.El("div", g.Iff(true, func() g.Node {
+			return g.El("span")
+		}))
+		assert.Equal(t, "<div><span></span></div>", n)
+	})
+
+	t.Run("returns nil if condition is false", func(t *testing.T) {
+		n := g.El("div", g.Iff(false, func() g.Node {
+			return g.El("span")
+		}))
+		assert.Equal(t, "<div></div>", n)
+	})
+}
+
+func ExampleIff() {
+	var nillableVariable *struct {
+		str string
+	}
+	e := g.El("div",
+		g.Iff(nillableVariable != nil, func() g.Node {
+			return g.Text(nillableVariable.str)
+		}),
+	)
+	_ = e.Render(os.Stdout)
+	// Output: <div></div>
+}