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

Implement http.Handler so that templates can be rendered directly #27

Closed
a-h opened this issue Oct 16, 2021 · 2 comments
Closed

Implement http.Handler so that templates can be rendered directly #27

a-h opened this issue Oct 16, 2021 · 2 comments

Comments

@a-h
Copy link
Owner

a-h commented Oct 16, 2021

At the moment, to output a template over HTTP, the template has to be wrapped by some boilerplate code.

func main() {
	http.Handle("/posts", PostHandler{})
	http.ListenAndServe(":8000", nil)
}

type PostHandler struct{}

func (ph PostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	posts := []Post{
		{
			Name:   "templ",
			Author: "author",
		},
	}
	err := postsTemplate(posts).Render(r.Context(), w)
	if err != nil {
		log.Println("error", err)
	}
}

If templ components implemented http.Handler, it could be shortened to:

func main() {
	http.Handle("/posts", postsTemplate([]Post{{ Name: "templ", Author: "author"}}))
	http.ListenAndServe(":8000", nil)
}

The component's ServeHTTP method would render a minimal error page, and return a 500 status code if the template failed to render.

In the case that the author of the template wanted to return a non HTTP 200 status code etc. then they'd use HTTP middleware to do it.

func main() {
	http.Handle("/404", withStatus(http.NotFound, errorTemplate404()))
	http.ListenAndServe(":8000", nil)
}

func withStatus(code int, next http.Handler) http.Handler {
	return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(code)
		next.ServeHTTP(w, r)
	})
}
@a-h
Copy link
Owner Author

a-h commented Oct 16, 2021

Rethink - by creating a separate ComponentHandler struct and a templ.Handler function that creates one, it's possible to support adding a status code without having developers deal with middleware.

// ComponentHandler is a http.Handler that renders components.
type ComponentHandler struct {
	Component    Component
	Status       int
	ErrorHandler http.Handler
}

// ServeHTTP implements the http.Handler interface.
func (ch ComponentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if ch.Status != 0 {
		w.WriteHeader(ch.Status)
	}
	if err := ch.Component.Render(r.Context(), w); err != nil {
		if ch.ErrorHandler != nil {
			ch.ErrorHandler.ServeHTTP(w, r)
			return
		}
		http.Error(w, "templ: failed to render template", http.StatusInternalServerError)
	}
}

// Handler creates a http.Handler that renders the template.
func Handler(c Component) ComponentHandler {
	return ComponentHandler{
		Component: c,
	}
}

Then, the usage is:

func main() {
	data := []Post{{ Name: "templ", Author: "author"}}
	http.Handle("/posts", templ.Handler(postsTemplate(data)))
	http.ListenAndServe(":8000", nil)
}

And to customise the status code:

func main() {
	notFoundHandler := templ.Handler(errorTemplate404())
	notFoundHandler.Status = http.NotFound
	http.Handle("/404", notFoundHandler)
	http.ListenAndServe(":8000", nil)
}

A better signature design for templ.Handler could make use of the functional options pattern (item 3 at https://golang.cafe/blog/golang-functional-options-pattern.html) to make it possible to set the status without another line.

@a-h
Copy link
Owner Author

a-h commented Oct 16, 2021

Fixed in e8737b3

@a-h a-h closed this as completed Oct 16, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant