Open mamereu opened 11 months ago
Not really, this concept is very different from the common template engins Believe it does not offer much added value to encapsulate it
I would like to see this too, but I'm not sure how it could be done in this repo because of the way go templ works. There needs to be a fiber->templ connector that is specific to the individual project's templ functions. I went down the path of making one.
The fiber view engine interface isn't complicated:
type Views interface {
Load() error
Render(io.Writer, string, interface{}, ...string) error
}
Load()
is called when the fiber app is first initialized. Render
is called inside the request. The parameters are: the output stream, the name of the template, the binding, the rest of the layout names.
To connect this with go templ, you need a struct with these methods implemented to call your templ functions. I would put it in the same package as the templ functions for convenience.
func NewTemplEngine() *TemplEngine {
return &TemplEngine{}
}
type TemplEngine struct {
}
func (te *TemplEngine) Load() error {
return nil
}
func (te *TemplEngine) Render(res io.Writer, templateName string, binding any, layouts ...string) error {
return nil
}
You can pass this to the fiber app like:
package main
import "your/app/views" // your templ dir
import "github.com/gofiber/fiber"
func main() {
app := fiber.New()
app := fiber.New(fiber.Config{
Views: views.NewTemplEngine(),
})
app.Get("/", func(c *fiber.Ctx) error {
return c.Render("Greeter", "World")
})
app.Listen(3000)
}
This will run, but it won't render anything. We have to marry the Greeter
template name with the Greeter
templ function.
Define our greeter templ:
package views
templ Greeter(name string) {
<span>Hello, { name }!</span>
}
We need to alter our Render
function so that it calls the right templ function:
func (te *TemplEngine) Render(res io.Writer, templateName string, binding any, layouts ...string) error {
switch templateName {
case "Greeter":
// coax our binding into the Greeter's string param type:
name, _ := binding.(string)
// call the templ function to get it's renderer
templRenderer := Greeter(name)
// render the templ function to the output stream
return templRenderer(context.Background(), res)
default:
return errors.New("template not found: " + templateName)
}
return nil
}
I think for it to be frictionless, there needs to be an additional tool that generates this code from the templ generated files. It makes more sense to me to put that in the github.com/a-h/templ project or make it independent from both projects.
If you really want to use templ in your fiber projects, I think it's a bit easier just to call the templ function in the fiber handler, and let templ do your layout:
func renderTempl(c *fiber.Ctx, cmpnt templ.Component) error {
content := new(bytes.Buffer)
cmpnt.Render(c.Context(), content) // maybe cmpnt.Render(c.UserContext(), content) ???
c.Set("Content-Type", "text/html")
return c.Send(content.Bytes())
}
app.Get("/", func(c *fiber.Ctx) error {
return renderTempl(c, views.Greeter("World"))
})
The following is working for me. Is it a good enough solution?
package main
import (
"log"
"github.com/a-h/templ"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/adaptor"
// This is the folder/package where `templ generate` puts the final templates on my project.
t "github.com/my/webdev-examples/template"
)
func main() {
app := fiber.New()
app.Get("/", render(t.Home()))
log.Fatal(app.Listen(":3000"))
}
// Use the Fiber adaptor middleware to turn a templ Handler into a fiber one.
func render(c templ.Component) fiber.Handler {
return adaptor.HTTPHandler(templ.Handler(c))
}
Thanks. I didn't know about adaptor.HTTPHandler
. I like your solution.
How would you handle this?
templ Greeting(name string) {
<p>{name}</p>
}
func main() {
app := fiber.New()
app.Get("/greeting/:name", render(t.Greeting("the :name parameter somehow?")))
log.Fatal(app.Listen(":3000"))
}
@luv2code I ran into that right after I posted the simple solution above. I haven't found a solution that's not over-engineered yet, let alone reach a simple and elegant one. So your solution is better and more complete. Although I hope for better fiber support that makes the solution simpler and more elegant.
Hi folks, author of templ here, since the ctx
type implements io.Writer
this also works.
package main
templ Hello(name string) {
<div>{ name }</div>
}
package main
import (
"log"
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New()
app.Get("/:name", func(c *fiber.Ctx) error {
c.Set("Content-Type", "text/html")
return Hello(c.Params("name")).Render(c.Context(), c)
})
log.Fatal(app.Listen(":3000"))
}
But... I'm not experienced with Fiber, so I don't know what the best approach is.
There's another suggestion here: https://github.com/a-h/templ/issues/349#issuecomment-1867650527
I would happily take a PR for a Fiber example, once we have consensus on best performance and nicest code. 😁
@luv2code - it shows how to use c.Params
to grab the parameter from the URL.
Thanks. I didn't know about
adaptor.HTTPHandler
. I like your solution.How would you handle this?
templ Greeting(name string) { <p>{name}</p> } func main() { app := fiber.New() app.Get("/greeting/:name", render(t.Greeting("the :name parameter somehow?"))) log.Fatal(app.Listen(":3000")) }
@luv2code you'd only need to abstract the adaptor into a render function and use it. This would also allow to pass status-codes to your render function.
func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
name := c.Params("name")
return Render(c, GreeterView(name))
})
app.Use(NotFoundMiddleware)
log.Fatal(app.Listen(":3000"))
}
func NotFoundMiddleware(c *fiber.Ctx) error {
return Render(c, NotFoundView(), templ.WithStatus(http.StatusNotFound))
}
func Render(c *fiber.Ctx, component templ.Component, options ...func(*templ.ComponentHandler)) error {
componentHandler := templ.Handler(component)
for _, o := range options {
o(componentHandler)
}
return adaptor.HTTPHandler(componentHandler)(c)
}
I think integrating this into fiber will not be necessary. Something like return c.Render("Greeter", "World")
where "Greeter" would be the component and "World" would be the argument(s) would ignore the benefit we have from templ generated go files.
I would happily take a PR for a Fiber example, once we have consensus on best performance and nicest code. 😁
I like @bastianwegge 's solution, and I nominate that for use in a fiber-templ integration example PR.
Agreed. It looks great. 😀
@bastianwegge, that's working great! I think it can be simplified even further, unless there is a reason I'm not seeing for not doing so:
func Render(c *fiber.Ctx, component templ.Component, options ...func(*templ.ComponentHandler)) error {
componentHandler := templ.Handler(component, options...)
return adaptor.HTTPHandler(componentHandler)(c)
}
This is because templ.Handler
takes a variadic ...options
and does the same thing in an internal loop:
Which is:
func Handler(c Component, options ...func(*ComponentHandler)) *ComponentHandler {
ch := &ComponentHandler{
Component: c,
ContentType: "text/html",
}
for _, o := range options {
o(ch)
}
return ch
}
cc: @a-h, @luv2code
I just went ahead and made a Templ middleware & a view helper function for my handlers / controllers ..
// Templ is a middleware function that sets up a Fiber middleware
func Templ() fiber.Handler {
return func(res *fiber.Ctx) error {
// Local allows you to store data in the request context within the request handler.
// Here, we define two local functions, "RenderComponent" and "Render".
// "RenderComponent" allows rendering a templated component with options.
res.Locals("RenderComponent", func(component templ.Component, options ...func(*templ.ComponentHandler)) error {
handler := templ.Handler(component)
for _, option := range options {
option(handler)
}
return adaptor.HTTPHandler(handler)(res)
})
// "Render" is an alias for "RenderComponent", making it more convenient to use.
res.Locals("Render", func(component templ.Component, options ...func(*templ.ComponentHandler)) error {
return Render(res, component, options...)
})
return res.Next()
}
}
func RenderComponent(res *fiber.Ctx, component templ.Component, options ...func(*templ.ComponentHandler)) error {
handler := templ.Handler(component)
for _, option := range options {
option(handler)
}
return adaptor.HTTPHandler(handler)(res)
}
func Render(res *fiber.Ctx, component templ.Component, options ...func(*templ.ComponentHandler)) error {
return RenderComponent(res, component, options...)
}
then in main.go or wherever you initialize your app
func main() {
app := fiber.New()
app.Use(middleware.Templ())
// all that jazz
and in the handler
func Home(res *fiber.Ctx) error {
// view := res.Locals("Render").(func(templ.Component, ...func(*templ.ComponentHandler)) error)
component := views.Home("Welcome")
// return view(component)
return view(res, component)
}
func view(res *fiber.Ctx, component templ.Component) error {
renderFunc := res.Locals("Render").(func(templ.Component, ...func(*templ.ComponentHandler)) error)
return renderFunc(component)
}
I opted for reusability and customization
@bastianwegge I was wondering if you could elaborate a bit more why you prefer using adaptor.HTTPHandler
, or what advantages it has over @a-h 's approach?
It seems that return Hello(c.Params("name")).Render(c.Context(), c)
works just as well. To make it more comfortable to use it could also be refactored into a func Render(fiber.Ctx, templ.Component)
. With a bit more code it would also allow setting custom status codes, etc. And it would save the adaptor's overhead?
But maybe there are usage scenarios that cannot be handled with it? Or is there some other drawback?
I don't intend to criticize your solution - it's nice and clean - I am just curious, trying to understand the pros and cons (if there are any).
With a bit more code it would also allow setting custom status codes, etc
I'm curious about this. Do you have anything to show? I went down this path a little but I didn't come up with anything as simple as the code below. I think eliminating a call/dependency would be great; but not at the expense of simplicity. For my use, the overhead of the adaptor.HTTPHandler
call (I'm not sure there is any) is not significant.
BTW, This is the solution I arrived at (it combines bastianwegge with andradei's improvement)
func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
name := c.Params("name")
return Render(c, GreeterView(name))
})
app.Use(NotFoundMiddleware)
log.Fatal(app.Listen(":3000"))
}
func NotFoundMiddleware(c *fiber.Ctx) error {
return Render(c, NotFoundView(), templ.WithStatus(http.StatusNotFound))
}
func Render(c *fiber.Ctx, component templ.Component, options ...func(*templ.ComponentHandler)) error {
componentHandler := templ.Handler(component, options...)
return adaptor.HTTPHandler(componentHandler)(c)
}
I did some benchmarks and the overhead of calling the adaptor is not completely insignificant, imo:
$> wrk -t100 -c400 -d30s http://localhost:3000/with
Running 30s test @ http://localhost:3000/with
100 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.42ms 2.07ms 82.38ms 87.70%
Req/Sec 5.65k 0.99k 42.45k 87.23%
16912859 requests in 30.10s, 2.24GB read
Requests/sec: 561899.89
Transfer/sec: 76.09MB
$> wrk -t100 -c400 -d30s http://localhost:3000/without
Running 30s test @ http://localhost:3000/without
100 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.46ms 2.04ms 42.24ms 86.33%
Req/Sec 6.33k 1.14k 48.54k 89.48%
18955448 requests in 30.10s, 2.24GB read
Requests/sec: 629800.26
Transfer/sec: 76.28MB
Given that one of the primary reasons to choose fiber is performance, I think this warrants further investigation.
Here is the code under test:
// main.go
package main
import (
"log"
. "test/views"
"github.com/a-h/templ"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/adaptor"
)
func main() {
app := fiber.New()
app.Get("/with", func(c *fiber.Ctx) error {
return Render(c, Greeter())
})
app.Get("/without", func(c *fiber.Ctx) error {
c.Set("Content-Type", "text/html")
return Greeter().Render(c.Context(), c)
})
log.Fatal(app.Listen(":3000"))
}
func Render(c *fiber.Ctx, component templ.Component, options ...func(*templ.ComponentHandler)) error {
componentHandler := templ.Handler(component, options...)
return adaptor.HTTPHandler(componentHandler)(c)
}
// views/greeter.templ
package views
templ Greeter() {
<span>Hello, World!</span>
}
// go.mod
module test
go 1.22.0
require (
github.com/a-h/templ v0.2.543
github.com/gofiber/fiber/v2 v2.52.0
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)
@bastianwegge I was wondering if you could elaborate a bit more why you prefer using
adaptor.HTTPHandler
, or what advantages it has over @a-h 's approach?It seems that
return Hello(c.Params("name")).Render(c.Context(), c)
works just as well. To make it more comfortable to use it could also be refactored into afunc Render(fiber.Ctx, templ.Component)
. With a bit more code it would also allow setting custom status codes, etc. And it would save the adaptor's overhead?But maybe there are usage scenarios that cannot be handled with it? Or is there some other drawback?
I don't intend to criticize your solution - it's nice and clean - I am just curious, trying to understand the pros and cons (if there are any).
No offense taken, I basically answered a question about how to support status-codes. IMHO I don't see a problem in the performance of the solution. If there is another solution that supports the same features and is even faster, I'd be happy to see that.
IMHO I don't see a problem in the performance of the solution.
I just mentioned the overhead because fasthttp docs mention it.
But I guess that it won't be noticeable for users of a lot of (or most?) applications - even if it may be measurable.
I mainly wanted to ask if there is some usage pattern that is only possible with adaptor.HTTPHandler
, and not with component.Render
.
Anyway, here's what I tried. Probably not as versatile as the options ...func(*templ.ComponentHandler)
but would you pass anything else than templ.WithStatus
like in the example?
package main
import (
"log"
"github.com/a-h/templ"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/adaptor"
)
func Adaptor(c *fiber.Ctx, component templ.Component, options ...func(*templ.ComponentHandler)) error {
componentHandler := templ.Handler(component, options...)
return adaptor.HTTPHandler(componentHandler)(c)
}
func Render(c *fiber.Ctx, component templ.Component, status int) error {
c.Status(status).Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
return component.Render(c.Context(), c)
}
func NotFoundMiddleware(c *fiber.Ctx) error {
// return Adaptor(c, NotFound(), templ.WithStatus(http.StatusNotFound))
return Render(c, NotFound(), fiber.StatusNotFound)
}
func getName(c *fiber.Ctx) string {
name := c.Params("name")
if name == "" {
name = "World"
}
return name
}
func main() {
app := fiber.New()
app.Get("/adaptor/:name?", func(c *fiber.Ctx) error {
name := getName(c)
return Adaptor(c, Home(name))
})
app.Get("/render/:name?", func(c *fiber.Ctx) error {
name := getName(c)
return Render(c, Home(name), fiber.StatusOK)
})
app.Use(NotFoundMiddleware)
log.Fatal(app.Listen(":3000"))
}
package main
templ Home(name string) {
<div>Hello { name }</div>
}
templ NotFound() {
<div>404</div>
}
Agree this shouldnt be done with the Adaptor middleware. It adds overhead which is not something you want when rendering templates.
The adaptor is good for things that are not called all the time, example "Serving a swagger yaml/json".
@luv2code Can you try doing the benchmark using golang
benchmarks instead of wrk
?
That way we can see how much overhead there is per operation and how much allocs are being made.
right, why does someone need the integration as a template engine, what advantages should that bring? the use is quite simple https://github.com/gofiber/template/issues/302#issuecomment-1867929456
comp/components.templ
package comp
templ Hello() {
<div>Hello</div>
}
main.go
package main
import (
"context"
"github.com/gofiber/fiber/v2"
"main/comp"
"log"
)
func main() {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
c.Type("html")
return comp.Hello().Render(context.Background(), c)
})
log.Fatalln(app.Listen(":3000"))
}
Feature Description
Any plan to include templ support ?
Additional Context (optional)
No response
Code Snippet (optional)
Checklist: