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

Why can't a group be rendered? #154

Closed
ddouglas opened this issue Nov 5, 2023 · 11 comments
Closed

Why can't a group be rendered? #154

ddouglas opened this issue Nov 5, 2023 · 11 comments

Comments

@ddouglas
Copy link

ddouglas commented Nov 5, 2023

Hello,

I use gocomponents with HTMX for my hobby sites and I'm currently working on a POC for a project at work.

I noticed today that the Group type satisfies the Node interface, but it cannot itself be directory rendered.


type group struct {
	children []Node
}

// String satisfies fmt.Stringer.
func (g group) String() string {
	panic("cannot render group directly")
}

// Render satisfies Node.
func (g group) Render(io.Writer) error {
	panic("cannot render group directly")
}

// 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.
func Group(children []Node) Node {
	return group{children: children}
}

I'd like to be able to use this to send multiple groups elements in a response for HTMX to perform an out of band swaps and the group method would allow this by looping over the nodes and writing them to the provided io.Writer

I'm more writing to ask what prompted this functionality to be designed to panic instead of rendering when the lib was written and is the advised work around to just write a function that loops and calls the Render func?

@ddouglas
Copy link
Author

ddouglas commented Nov 5, 2023

An alternative that i'd also be excited about is to export the group type and it's children attribute so that we can use Group func programmatically and then access the underlying children to do the rendering ourselves

@markuswustenberg
Copy link
Member

Hi @ddouglas. Thank you for raising this issue!

Basically, it's because rendering a group of Nodes without a parent element can be nonsensical, such as rendering attributes outside an element. Thus, a group could be rendered to class="foo"><span></span> or something like that, which I didn't want to support. In this way, the rendering is always handled inside the parent element, but the Node interface still needs to be satisfied. Perhaps it would make sense to lax this restriction to only panic on root-level attributes. I'll have to think about the consequences of that though.

I don't know about exporting it, I really like to keep the core API surface area minimal. Would you be able to achieve something similar with nested div elements or similar?

@ddouglas
Copy link
Author

Thanks for clearing this up. That makes sense.

Unfortunately, wrapping it in a div violates the requirements of performing an out of band swap has each id that needs to be swapped must be a top level element, but returning multiple "top level" elements that aren't wrapped by a div still satisfies this contract:

<!-- This is okay -->
<div id="a">
</div>
<div id="b">
</div>

<!-- This is not -->
<div>
   <div id="a">
   </div>
   <div id="b">
   </div>
</div>

Regardless, we (i) don't expect you to make sweeping changes to the lib because HTMX is picking up steam this year. Thank you for this awesome lib.

@markuswustenberg
Copy link
Member

@ddouglas I'm glad you found a solution. I'm an HTMX fan myself, so maybe there's room for some helpers over at https://github.com/maragudk/gomponents-htmx 😊 . Let me know if you think of something.

@eduardolat
Copy link

eduardolat commented Jan 21, 2024

I ran into exactly the same problem when trying to make pagination with HTMX, I made a help function for whoever needs it

package components

import (
	"bytes"

	"github.com/maragudk/gomponents"
)

// RenderableGroup renders a group of nodes without a parent element.
//
// This is because gomponents.Group() cannot be directly rendered and
// needs to be wrapped in a parent element.
func RenderableGroup(children []gomponents.Node) gomponents.Node {
	buf := bytes.Buffer{}
	for _, child := range children {
		err := child.Render(&buf)
		if err != nil {
			return gomponents.Raw("Error rendering group")
		}
	}
	return gomponents.Raw(buf.String())
}

And the tests:

package components

import (
	"bytes"
	"testing"

	"github.com/maragudk/gomponents"
	"github.com/maragudk/gomponents/html"
	"github.com/stretchr/testify/assert"
)

func TestRenderableGroupRenderer(t *testing.T) {
	t.Run("renders a group of string nodes without a parent element", func(t *testing.T) {
		gotRenderer := RenderableGroup([]gomponents.Node{
			gomponents.Text("foo"),
			gomponents.Text("bar"),
		})

		got := bytes.Buffer{}
		err := gotRenderer.Render(&got)
		assert.NoError(t, err)

		expected := "foobar"

		assert.Equal(t, expected, got.String())
	})

	t.Run("renders a group of tag nodes without a parent element", func(t *testing.T) {
		gotRenderer := RenderableGroup([]gomponents.Node{
			html.Span(
				gomponents.Text("foo"),
			),
			html.P(
				gomponents.Text("bar"),
			),
		})

		got := bytes.Buffer{}
		err := gotRenderer.Render(&got)
		assert.NoError(t, err)

		expected := `<span>foo</span><p>bar</p>`

		assert.Equal(t, expected, got.String())
	})
}

@JulienTant
Copy link
Contributor

JulienTant commented Jun 21, 2024

I ended up doing something like what @eduardolat did. I wish we'd have a Group.UNSAFERender(). This let the user know they are doing something that can be shady, but it does let them do it if they need to. And with HTMX growing, I think rendering groups is more relevant than ever 😅

@markuswustenberg
Copy link
Member

@JulienTant agreed. I often wish for this myself in my code, because I use HTMX myself. I think I just want to make Group renderable, and then add a disclaimer that this can be used wrong. Or maybe drop root-level attributes silently, I haven't decided yet. Tracking this in #162.

@eduardolat
Copy link

@markuswustenberg +1 to make group renderable + disclaimer

@markuswustenberg
Copy link
Member

See #181.

@amrojjeh
Copy link
Contributor

amrojjeh commented Jun 26, 2024

Personally I like to assemble my fragments at the handler, which means I do something like this:

	page.Question(question.Prompt,
		components.QuestionToInputMethod(question, questionSession),
		feedback,
		questionURLHTMX(quiz.ID, question.Position-1, len(questions)),
		questionURLHTMX(quiz.ID, question.Position+1, len(questions)),
		questionURL(quiz.ID, question.Position, len(questions)),
	).Render(w)
	page.Sidebar(true, sidebar(true, quiz.ID, questionSessions, questions), summaryURL(quiz.ID)).Render(w)
	components.Excerpt(true, excerpt, question.Reference).Render(w)

I just render out several elements onto the ResponseWriter, which is pretty much the equivalent of a fragment. Plus it lets me be more versatile in which fragments I respond with (yes the code needs some refactoring but the idea is there)

@markuswustenberg
Copy link
Member

@amrojjeh Ah, interesting approach! I hadn't thought of that. I use the HTTP handler adapter extensively, and barely ever render directly, so for me the Fragment will come in useful I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants