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

🚀 [Feature]: Templ support? #302

Open
3 tasks done
mamereu opened this issue Oct 14, 2023 · 19 comments
Open
3 tasks done

🚀 [Feature]: Templ support? #302

mamereu opened this issue Oct 14, 2023 · 19 comments

Comments

@mamereu
Copy link

mamereu commented Oct 14, 2023

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:

  • I agree to follow Fiber's Code of Conduct.
  • I have checked for existing issues that describe my suggestion prior to opening this one.
  • I understand that improperly formatted feature requests may be closed without explanation.
@ReneWerner87
Copy link
Member

Not really, this concept is very different from the common template engins
Believe it does not offer much added value to encapsulate it

@luv2code
Copy link

luv2code commented Oct 28, 2023

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
Copy link

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
Copy link

luv2code commented Nov 11, 2023

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
Copy link

@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
Copy link

a-h commented Dec 22, 2023

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: 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 c.Params to grab the parameter from the URL.

@bastianwegge
Copy link

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
Copy link

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
Copy link

a-h commented Dec 22, 2023

Agreed. It looks great. 😀

@andradei
Copy link

@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

@gaby gaby changed the title Templ support?🚀 [Feature]: 🚀 [Feature]: Templ support? Jan 8, 2024
@dkcheun
Copy link

dkcheun commented Jan 18, 2024

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
Copy link

@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
Copy link

luv2code commented Feb 7, 2024

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
Copy link

luv2code commented Feb 7, 2024

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
Copy link

@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
Copy link

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
Copy link
Member

gaby commented Feb 18, 2024

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
Copy link
Member

gaby commented Feb 18, 2024

@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
Copy link
Member

right, why does someone need the integration as a template engine, what advantages should that bring?
the use is quite simple
#302 (comment)

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

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

9 participants