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

Allow components to edit context key-value pairs #603

Closed
cmar0027 opened this issue Mar 10, 2024 · 5 comments
Closed

Allow components to edit context key-value pairs #603

cmar0027 opened this issue Mar 10, 2024 · 5 comments

Comments

@cmar0027
Copy link

It would be nice if components were able to edit the context key value pairs in the Render() method.
I couldn't find a way to do this, but I think it would be nice for a variety of use cases, so I'd like to know if anyone has found a workaround or if other people are also looking forward to such a feature.

The following is an example use case to better illustrate what I'm talking about:

Imagine you have a page with a few footnotes. It's tedious and error prone to manually make sure each footnote is numbered correctly and in order, especially if there is a bunch of them and they change often.
Instead, it would be nice to just call a component, say @FootNote, to dinamically create and number footnotes wherever you want across the page, for example @FootNote("This is footnote 1") would spit [1] out, then @FootNote("This is footnote 2") would spit [2] out somewhere else, and so on.
Finally, calling @FootNotes() at the end of the page would spit out the list of all footnotes in the page:

[1] This is footnote 1
[2] This is footnote 2

It would be easy to implement something like this if components were able to edit context values, this way @FootNote could just add the given content to the context, then @FootNotes would simply need to read and display that list.

Here is an implementation that almost works:

package views

import "fmt"

const FOOT_NOTE_CTX_KEY = "foot_notes_key"
const FOOT_NOTE_ID_PREFIX = "foot-note-"

// FootNote renders a footnote link, e.g. [1]
func FootNote(content string) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
		footNotes, ok := ctx.Value(FOOT_NOTE_CTX_KEY).([]string)
		if !ok {
			footNotes = []string{}
		}
		footNotes = append(footNotes, content)
                // PROBLEM: the new context doesn't override the global context, only the local reference
		ctx = context.WithValue(ctx, FOOT_NOTE_CTX_KEY, footNotes)

		n := len(footNotes)
		component := footNoteComponent(n)
		return component.Render(ctx, w)
	})
}

// FootNotes renders all footnotes
func FootNotes() templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {

		footNotes, ok := ctx.Value(FOOT_NOTE_CTX_KEY).([]string)
		if !ok {
			footNotes = []string{}
		}

		component := footNotesComponent(footNotes)

		return component.Render(ctx, w)
	})
}

templ footNoteComponent(n int) {
	<a class="underline" href={ templ.SafeURL(fmt.Sprint("#", FOOT_NOTE_ID_PREFIX, n)) }>[{ fmt.Sprint(n) }]</a>
}

templ footNotesComponent(notes []string) {
	<div>
		<h2>Foot Notes</h2>
		<ul>
			for i, note := range notes {
				<li id={ fmt.Sprintf("%s%d", FOOT_NOTE_ID_PREFIX, i+1) }>
					[{ fmt.Sprint(i+1) }] { note }
				</li>
			}
		</ul>
	</div>
}

The problem is that @FootNotes() doesn't output the full list of footnotes because ctx = context.WithValue(ctx, FOOT_NOTE_CTX_KEY, footNotes) doesn't update the passed in context, just its local copy.

I think something like this would enable a lot of interesting use cases for when a piece of content in the page dependes on another piece of content earlier in the page, that you either don't know in advanced, or are too lazy to manually setup before rendering.

@a-h
Copy link
Owner

a-h commented Mar 10, 2024

I haven't had time to fully digest this, but it is possible to update the context from within a templ component, if the value is already available in the context to mutate, as per: https://go.dev/play/p/GtvBP0VICp4

package main

import (
	"bytes"
	"context"
	"fmt"
	"html"
	"io"

	"github.com/a-h/templ"
)

func Footnote(name, href string) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
		// Get the footnote context and update it.
		fc := ctx.Value(FootnoteContextKey{}).(*FootnoteContext)
		fc.Footnotes = append(fc.Footnotes, FootnoteData{Name: name, Href: href})

		// Render the footnote name immediately.
		io.WriteString(w, html.EscapeString(name)+"\n")
		return nil
	})
}

type FootnoteContextKey struct{}

type FootnoteData struct {
	Name string
	Href string
}

type FootnoteContext struct {
	Footnotes []FootnoteData
}

func PrepareFootnoteContext(ctx context.Context) context.Context {
	return context.WithValue(ctx, FootnoteContextKey{}, &FootnoteContext{
		Footnotes: make([]FootnoteData, 0),
	})
}

func FootnoteFooter() templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
		fc := ctx.Value(FootnoteContextKey{}).(*FootnoteContext)
		io.WriteString(w, "\n<footer>\n")
		for i, f := range fc.Footnotes {
			io.WriteString(w, fmt.Sprintf("<div>\n\t%d <a href=\"%s\">%s</a>\n</div>\n", i+1, html.EscapeString(f.Href), html.EscapeString(f.Name)))
		}
		io.WriteString(w, "</footer>")
		return nil
	})
}

func main() {
	ctx := context.Background()
	ctx = PrepareFootnoteContext(ctx)

	w := bytes.NewBuffer(nil)
	Footnote("First footnote", "#first").Render(ctx, w)
	Footnote("Second footnote", "#second").Render(ctx, w)
	FootnoteFooter().Render(ctx, w)

	fmt.Println(w.String())
}

The output of the above is:

First footnote
Second footnote

<footer>
<div>
        1 <a href="#first">First footnote</a>
</div>
<div>
        2 <a href="#second">Second footnote</a>
</div>
</footer>

@cmar0027
Copy link
Author

Thank for your reply, I think this works, although having to setup the context value beforehand for each request could become a bit annoying if there are many specific contexts that need to be setup.

But I think this example could be turned into something very similar to what I was originally asking for by just storing a pointer to a second context inside the original one. Then anyone would be able to access it and modify it as needed.

@sirgallifrey
Copy link
Contributor

@cmar0027 I'm doing something similar on my little project I'm using to learn go and templ.
I have components call a function that will register which dependencies they need, and at the end, it will render the script tag for each dependency that was flagged.

It's in essence the same idea proposed by a-h.

I have a Middleware that will add a pointer to the context. (This is using fiber, but should work the same for a normal context, with just a little more code.)
https://github.com/sirgallifrey/gofinance/blob/main/web/view/ctx_middleware.go#L7

this is my struct, you can see I have useful things like the path and the environment, and also a map for the dependencies, if they are not used at all, it will be nil and not consume any memory.
that's why I think you don't need to setup a different context for each of the things you want to expose. Just add all at this one view context, unless you really want to separate some concerns that should not be together...
https://github.com/sirgallifrey/gofinance/blob/main/web/view/ctx.go#L9

you can see that I have some nice functions here to extract the values from the context here so I don't have to write this code inside the templates.

here I have one component flagging that HTMX is required and then it will be rendered here if it was actually required at least once.

Here is the script template, see how it gets the contexts and decides to render or not the script

You can't just execute functions inside a normal templ Component, so that function I have to flag the dependencies is actually a component that returns nil but gives me the chance to put the info in my struct

Hope that helps.

@joerdav
Copy link
Collaborator

joerdav commented May 14, 2024

I could be missing the point here but this suggestion seems to be in conflict with what ctx in Go tries to achieve. I think that if you are wanting to do this then the best option is to create a context value which is defined higher up and can be modified via reference, and this should be supported already. That could be a workaround.

However, in my personal opinion this would indicate to me that there is too much logic in my templates. I would likely create some view models for my page that contain all of the footnotes, pre-calculated before I even call into a templ component. Which may be more code and not fit with your current pattern.

Final suggestion is to not use ctx at all, you could define your own footnotes store type that gets passed down through templates, this would be more explicit as to how things are working. I hope some of these ideas help!

@joerdav
Copy link
Collaborator

joerdav commented May 31, 2024

The introduction of arbitrary go in templates should make this possible: https://templ.guide/syntax-and-usage/raw-go

I think now it is just a case of using Go to share this state upwards if that is what you would like!

@joerdav joerdav closed this as completed May 31, 2024
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

4 participants