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

📝 [Proposal]: We need partial render! #3297

Open
3 tasks done
vmpartner opened this issue Jan 29, 2025 · 12 comments
Open
3 tasks done

📝 [Proposal]: We need partial render! #3297

vmpartner opened this issue Jan 29, 2025 · 12 comments

Comments

@vmpartner
Copy link

Feature Proposal Description

We have amazing gofiber + htmx + sse. We can send partial template on each update via SSE
Example how it can be reached now:

app.Get("/", func (c fiber.Ctx) error {
  return c.SendStreamWriter(func(w *bufio.Writer) {
   for {
    data := getLiveData()
    buf := new(bytes.Buffer)
    c.App().Config().Views.Render(buf, "my-live-partial-template", data) // hack for partial render
    w.WriteString(buf)
    time.Sleep(5*time.Second)
   }
  })
})

I propose that we can add method RenderToWriter(w *bufio.Writer, templateName string, data fiber.Map, layoutName string)

app.Get("/", func (c fiber.Ctx) error {
  return c.SendStreamWriter(func(w *bufio.Writer) {
   for {
    data := getLiveData()
    c.RenderToWriter(w, "my-live-partial-template", data)
    time.Sleep(5*time.Second)
   }
  })
})

Alignment with Express API


HTTP RFC Standards Compliance


API Stability


Feature Examples

app.Get("/", func (c fiber.Ctx) error {
  return c.SendStreamWriter(func(w *bufio.Writer) {
   for {
    data := getLiveData()
    c.RenderToWriter(w, "my-live-partial-template", data)
    time.Sleep(5*time.Second)
   }
  })
})

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have searched for existing issues that describe my proposal before opening this one.
  • I understand that a proposal that does not meet these guidelines may be closed without explanation.
Copy link

welcome bot commented Jan 29, 2025

Thanks for opening your first issue here! 🎉 Be sure to follow the issue template! If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@grivera64
Copy link
Member

grivera64 commented Jan 29, 2025

+1 This sounds like a great idea. Due to the way SSE and c.SendStreamWriter() works, this might require two separate endpoints to work but seems do-able. Do you have a full working example using c.SendStreamWriter() on hand?

A problem we could potentially have with this is that a fiber.Ctx isn't considered safe to use from inside of c.SendStreamWriter() (not sure if this is mentioned in the docs). Reading could potentially be fine, but writing will cause issues from my experience working on the SendStreamWriter feature. If we were to add this method to the fiber.Ctx interface, then it may give a false impression that other Ctx methods would work here too (including ones that write to the response).

If possible, we may want to add this functionality to a different interface to avoid that confusion.

@JIeJaitt
Copy link
Contributor

@grivera64 @vmpartner Should we implement this functionality out as a new middleware?

@vmpartner
Copy link
Author

I think method RenderToWriter must appear inside main app, it cannot be implement in middleware

@grivera64
Copy link
Member

grivera64 commented Feb 28, 2025

@grivera64 @vmpartner Should we implement this functionality out as a new middleware?

I think it is possible to use a middleware to encapsulate the feature, though we would still need to implement a RenderToXXX() function of some sort like @vmpartner said.

An idea could be to create this method under a separate package (or even bundle it alongside the middleware in a partials package) to either render the template:

  • as a string/[]byte that can be sent over the network
partials.RenderAsBytes(templateName string) []byte
  • into a *bufio.Writer so we can use it directly like in the above
partials.RenderToWriter(w *bufio.Writer, templateName string) error

This way, we could directly use this in a middleware:

app.Get("/partial", partials.New("my-partial", func() fiber.Map {
  return getLiveData() // assuming getLiveData() returns a bind of type fiber.Map
})

What do you both think about this setup? @vmpartner @JIeJaitt

@JIeJaitt
Copy link
Contributor

JIeJaitt commented Mar 4, 2025

@grivera64 Sounds good!

@vmpartner
Copy link
Author

vmpartner commented Mar 5, 2025 via email

@grivera64
Copy link
Member

Dear @grivera64 https://github.com/grivera64, as i know we must work with
engine, its init with fiber app. I cant imagine how to use middleware for
it.

Yes, you are right @vmpartner. I forgot to mention about how we would provide the middleware with access to the template engine. The proposal above should be modified to factor that in.

With that in mind, we could potentially do one of the following:

  1. Directly access from Ctx like in @vmpartner's example
func New(name string, bindFunc func() fiber.Map) fiber.Handler {
    return func(c fiber.Ctx) error {
        // Get Engine from fiber.Ctx (since ctx may be deallocated/reused elsewhere
        // when the streamWriter executes)
        engine := c.App().Config().Views

        // Use engine here
        return c.SendStreamWriter(func(w *bufio.Writer) error {
            for {
                // Get the current bind
                bind := bindFunc()

                // ...

                // Wait before next request (e.g. Timeout, Some event)
                // ...
            }
        })
    }
}
  1. Create a partials.Renderer that is initialized in the same way that fiber.App is, where the user must also pass the engine there too.
engine := ...
app := fiber.New(fiber.Config{
    Views: engine,
    // Other configs ...
})
partialRenderer := partials.New(partials.Config{
    Views: engine,
})

app.Get("/partial", partialRenderer.Render("my-partial", func() fiber.Map {
  return getLiveData() // assuming getLiveData() returns a bind of type fiber.Map
})
  1. Add a new fiber.Ctx function (e.g. c.RenderPartial()) that works similarly to c.Render(). The middleware can use this function, or (as @vmpartner suggested) we can skip the middleware altogether:
func (c *DefaultCtx) RenderPartial(name string, bindFunc func() fiber.Map, layouts ...string) error {
    // Set headers for SSE...

    return c.SendStreamWriter(func(w *bufio.Writer) error {
        for {
            // Get the current bind
            bind := bindFunc()

            // Do the same from L1375 to L1426 in fiber/ctx.go
            // to minimize memory allocations
            // ...

            // Write to the buffer with the result from the template engine
            w.Write(buf.Bytes())

            // Wait before next request (e.g. Timeout, Some event)
            // ...
        }
    })
}

Out of these three, I like both 2 and 3. 1 may not work for the long run if DefaultCtx is refactored to work differently. 2 and 3 should both be viable options, but 3 requires less involved setup over 2 for the user, while 2 can make it easier to distinguish that a partial should be used on a separate handler instead of being called in the same handler as a full template.

Another issue is deciding how would this feature allow a user to specify when it should call bindFunc (e.g. Use a time interval, allow the user specify how themselves within the function). I am not sure how a user specified way would work, but if we were to instead provide an add-on (instead of a middlware) in a similar manner to 2, we could make this work similar to what @vmpartner proposed in the issue description:

engine := ...
app := fiber.New(fiber.Config{
    Views: engine,
    // Other configs ...
})
partialRenderer := partials.New(partials.Config{
    Views: engine,
})

app.Get("/", func (c fiber.Ctx) error {
    return c.SendStreamWriter(func(w *bufio.Writer) {
        for {
            data := getLiveData()
            partialRenderer.RenderToWriter(w, "my-live-partial-template", data)
            time.Sleep(5*time.Second)
        }
    })
})

What are your thoughts on these approaches?

@vmpartner
Copy link
Author

Sorry, hard to understand this cases for me. I understand this approach, seems it will work (with limitations):

engine := ...
app := fiber.New(fiber.Config{
    Views: engine,
    // Other configs ...
})
partialRenderer := partials.New(partials.Config{
    Views: engine,
})

app.Get("/", func (c fiber.Ctx) error {
    return c.SendStreamWriter(func(w *bufio.Writer) {
        for {
            data := getLiveData()
            partialRenderer.RenderToWriter(w, "my-live-partial-template", data)
            time.Sleep(5*time.Second)
        }
    })
})

but i preferer new func inside core

app.Get("/", func (c fiber.Ctx) error {
  return c.SendStreamWriter(func(w *bufio.Writer) {
   for {
    data := getLiveData()
    c.RenderToWriter(w, "my-live-partial-template", data)
    time.Sleep(5*time.Second)
   }
  })
})

its need not only for SSE, but in many other cases, when you need render email template for example.

@grivera64
Copy link
Member

Sorry, hard to understand this cases for me.

No worries. It's a rough sample, so there's a lot of details missing but I was trying to give the main ideas for the feature. Essentially, ideas 1 and 2 would be to implement middleware that is explicitly for making an SSE endpoint that will return a partial and fill it in using a bind from a user-provided function (it would contain logic to build the bind) just like in c.Render().

For example:

app.Get("/partial", partials.Render("my-live-partial-template"), func() fiber.Map {
  return getLiveData()
}, 5 * time.Second)

where partials.Render() has the signature:

func Render(name string, bindFunc fiber.Map, timeout time.Duration)

Would create the following equivalent handler:

app.Get("/partial", func(c fiber.Ctx) error {
    // Set headers accordingly
    // ...

    // Get the View from config
    view := c.App().Config().Views

    // Name of the partial
    name := "my-live-partial-template"

    // Bind Map Function gets the live data
    bindMap := func() fiber.Map {
        return getLiveData()
    }

    // Timeout
    timeout := 5*time.Second

    return c.SendStreamWriter(func(w *bufio.Writer) error {
        view.Render(w, name, bindFunc())
        time.Sleep(timeout)
    })
})

but i preferer new func inside core

app.Get("/", func (c fiber.Ctx) error {
  return c.SendStreamWriter(func(w *bufio.Writer) {
   for {
    data := getLiveData()
    c.RenderToWriter(w, "my-live-partial-template", data)
    time.Sleep(5*time.Second)
   }
  })
})

We could add a RenderToWriter() function (or similar function) to core instead of as an add-on, but we can not attach it to fiber.Ctx if we want to be able to use it within c.SendStreamWriter(). A fiber context is only valid within a handler, and is recycled after the return statement.

Since c.SendStreamWriter() runs after the handler returns, it is undefined behavior to use a fiber.Ctx within the streamWriter function. It works here since even after the context is recycled, the c.Config() will be the same for all pooled contexts, but it might give the false impression that other c.XXX() methods can be called from within c.SendStreamWriter().

its need not only for SSE, but in many other cases, when you need render email template for example.

In this case, would it make more sense to instead make it easier to obtain a View from the fiber.Ctx? For example:

app.Get("/", func (c fiber.Ctx) error {
  view := c.App().Config().Views

  return c.SendStreamWriter(func(w *bufio.Writer) {
    for {
      data := getLiveData()
      view.Render(w, "my-live-partial-template", data)
      time.Sleep(5*time.Second)
    }
  })
})

could become:

app.Get("/", func (c fiber.Ctx) error {
  view := c.View()

  return c.SendStreamWriter(func(w *bufio.Writer) {
    for {
      data := getLiveData()
      view.Render(w, "my-live-partial-template", data)
      time.Sleep(5*time.Second)
    }
  })
})

This way, we make it much simpler for users to access a view for any of their needs, though this change itself wouldn't implement a partial renderer on its own. A middleware or add-on would better simplify the process, but I am not sure if such an approach would be too limiting.

@vmpartner
Copy link
Author

vmpartner commented Mar 7, 2025 via email

@grivera64
Copy link
Member

No problem!

In this case, should we rename/create a separate subissue to track the get views proposal? We can also still consider the partials middleware for a simpler approach for a partial render, though other use cases of Views can still be doable through the 1st get views proposal.

  1. Add c.GetViews() (or similar function name) for easy template View/engine access from within fiber.Ctx. (For all use cases that need to get the provided Views engine).

  2. Add Partial Renderer Middleware (e.g. via partials.New() or partials.Render(name string, bindFunc fiber.Map, timeout time.Duration) fiber.Handler)

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

No branches or pull requests

3 participants