gofiber / template

🧬 Template engine middleware for Fiber
https://docs.gofiber.io/guide/templates
MIT License
260 stars 52 forks source link

🚀 [Feature]: Templ support? #302

Open mamereu opened 11 months ago

mamereu commented 11 months ago

Feature Description

Any plan to include templ support ?

Additional Context (optional)

No response

Code Snippet (optional)

package main

import "github.com/gofiber/template/%package%"

func main() {
  // Steps to reproduce
}

Checklist:

ReneWerner87 commented 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

luv2code commented 10 months ago

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"))
})
andradei commented 10 months ago

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))
}
luv2code commented 10 months ago

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"))
}
andradei commented 10 months ago

@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.

a-h commented 9 months ago

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.

bastianwegge commented 9 months ago

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.

luv2code commented 9 months ago

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.

a-h commented 9 months ago

Agreed. It looks great. 😀

andradei commented 8 months ago

@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

dkcheun commented 8 months ago

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

heapifyman commented 7 months ago

@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).

luv2code commented 7 months ago

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)
}
luv2code commented 7 months ago

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 commented 7 months ago

@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).

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.

heapifyman commented 7 months ago

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>
}
gaby commented 7 months ago

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".

gaby commented 7 months ago

@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.

ReneWerner87 commented 7 months ago

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"))
}

image