-
Notifications
You must be signed in to change notification settings - Fork 54
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
🚀 [Feature]: Templ support? #302
Comments
Not really, this concept is very different from the common template engins |
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
}
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 Define our greeter templ: package views
templ Greeter(name string) {
<span>Hello, { name }!</span>
} We need to alter our 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 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. |
Hi folks, author of templ here, since the 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: a-h/templ#349 (comment) 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 |
@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 |
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 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
} |
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 It seems that 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). |
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 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)
}
// 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
) |
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. |
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 Anyway, here's what I tried. Probably not as versatile as the
|
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 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? 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:
The text was updated successfully, but these errors were encountered: