Skip to content

Instantly share code, notes, and snippets.

@madhums
Last active August 18, 2022 07:53
Show Gist options
  • Save madhums/4340cbeb36871e227905 to your computer and use it in GitHub Desktop.
Save madhums/4340cbeb36871e227905 to your computer and use it in GitHub Desktop.
render html templates in gin (go)

GoDoc

Gin is a very good library, with a great performance! I was trying it out and learned that a lot of people were struggling with templating (as I did myself). gin-contrib's multitemplate is pretty good. But I think it lacks small things like debug mode, support for structured template rendering. Of-course, multitemplate should not be dictating how to structure your templates. But I think it makes sense to support a standard layout structure in a contrib package.

So I modified the multitemplate a bit, may be this can be called as multitemplate-extras? (tried to highlight some benefits).

The goal is to make html template rendering in gin simple and straight forward without having to write much code.

Benifits

  1. You don't have to add and parse files in each handler
  2. Supports debug mode. This allows you to visualize the change you did by simply refreshing the page without restarting the server.
  3. Simple rendering syntax for the template
// suppose `templates/articles/list.html` is your file to be rendered
c.HTML(http.StatusOK, "articles/list", "")
  1. More structured templates directory where you can place entities in logical units.
  2. Configure layout file
  3. Configure template file extension
  4. Configure templates directory
  5. Feels friendlier for people coming from communities like rails, express or django.
  6. All of this without loosing any performance of gin!

Usage

router := gin.Default()

// Set html render options
htmlRender := GinHTMLRender.New()
htmlRender.Debug = gin.IsDebugging()
htmlRender.Layout = "layouts/default"
// htmlRender.TemplatesDir = "templates/" // default
// htmlRender.Ext = ".html"               // default

// Tell gin to use our html render
router.HTMLRender = htmlRender.Create()

Suppose your structure is

|-- templates/
    |-- 
    |-- 400.html
    |-- 404.html
    |-- layouts/
        |--- default.html
    |-- articles/
        |--- list.html          
        |--- form.html

And if you want to render templates/articles/list.html in your handler

c.HTML(http.StatusOK, "articles/list", "")

See it in action here, here and here and a full demo with the source

Note

  1. Error is thrown if either of the layout file or templates directory doesn't exist
  2. Only one extension can be configured
  3. Only one layout fine can be used

Todo

  • Provide options to customize
    • template dir
    • layout
    • template extensions
    • includes dir
    • add template helpers
  • Add string and glob option back
  • Tests

Further steps...

I would like to know the thoughts of the community about this.

And based on that we have the following options I guess:

  1. Make this part of contrib's multitemplate
  2. Add this as another contrib module, say multitemplate_extras
  3. Leave the above, I will make another repo :)
// Package GinHTMLRender provides some sugar for gin's template rendering
//
// This work is based on gin contribs multitemplate render https://github.com/gin-gonic/contrib/blob/master/renders/multitemplate
//
// Usage
//
// router := gin.Default()
//
// // Set html render options
// htmlRender := GinHTMLRender.New()
// htmlRender.Debug = gin.IsDebugging()
// htmlRender.Layout = "layouts/default"
// // htmlRender.TemplatesDir = "templates/" // default
// // htmlRender.Ext = ".html" // default
//
// // Tell gin to use our html render
// router.HTMLRender = htmlRender.Create()
//
// Structure
//
// |-- templates/
// |--
// |-- 400.html
// |-- 404.html
// |-- layouts/
// |--- default.html
// |-- articles/
// |--- list.html
// |--- form.html
//
//
// And if you want to render `templates/articles/list.html` in your handler
//
// c.HTML(http.StatusOK, "articles/list", "")
//
package GinHTMLRender
import (
"html/template"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin/render"
)
const (
// TemplatesDir holds the location of the templates
TemplatesDir = "templates/"
// Layout is the file name of the layout file
Layout = "layout"
// Ext is the file extension of the rendered templates
Ext = ".html"
// Debug enables debug mode
Debug = false
)
// Render implements gin's HTMLRender and provides some sugar on top of it
type Render struct {
Templates map[string]*template.Template
Files map[string][]string
TemplatesDir string
Layout string
Ext string
Debug bool
}
// Add assigns the name to the template
func (r *Render) Add(name string, tmpl *template.Template) {
if tmpl == nil {
panic("template can not be nil")
}
if len(name) == 0 {
panic("template name cannot be empty")
}
r.Templates[name] = tmpl
}
// AddFromFiles parses the files and returns the result
func (r *Render) AddFromFiles(name string, files ...string) *template.Template {
tmpl := template.Must(template.ParseFiles(files...))
if r.Debug {
r.Files[name] = files
}
r.Add(name, tmpl)
return tmpl
}
// Instance implements gin's HTML render interface
func (r *Render) Instance(name string, data interface{}) render.Render {
var tpl *template.Template
// Check if gin is running in debug mode and load the templates accordingly
if r.Debug {
tpl = r.loadTemplate(name)
} else {
tpl = r.Templates[name]
}
return render.HTML{
Template: tpl,
Data: data,
}
}
// loadTemplate parses the specified template and returns it
func (r *Render) loadTemplate(name string) *template.Template {
tpl, err := template.ParseFiles(r.Files[name]...)
if err != nil {
panic(name + " template name mismatch")
}
return template.Must(tpl, err)
}
// New returns a fresh instance of Render
func New() Render {
return Render{
Templates: make(map[string]*template.Template),
Files: make(map[string][]string),
TemplatesDir: TemplatesDir,
Layout: Layout,
Ext: Ext,
Debug: Debug,
}
}
// Create goes through the `TemplatesDir` creating the template structure
// for rendering. Returns the Render instance.
func (r *Render) Create() *Render {
r.Validate()
layout := r.TemplatesDir + r.Layout + r.Ext
// root dir
tplRoot, err := filepath.Glob(r.TemplatesDir + "*" + r.Ext)
if err != nil {
panic(err.Error())
}
// sub dirs
tplSub, err := filepath.Glob(r.TemplatesDir + "**/*" + r.Ext)
if err != nil {
panic(err.Error())
}
for _, tpl := range append(tplRoot, tplSub...) {
// This check is to prevent `panic: template: redefinition of template "layout"`
name := r.getTemplateName(tpl)
if name == r.Layout {
continue
}
r.AddFromFiles(name, layout, tpl)
}
return r
}
// Validate checks if the directory and the layout files exist as expected
// and configured
func (r *Render) Validate() {
// add trailing slash if the user has forgotten..
if !strings.HasSuffix(r.TemplatesDir, "/") {
r.TemplatesDir = r.TemplatesDir + "/"
}
// check for templates dir
if ok, _ := exists(r.TemplatesDir); !ok {
panic(r.TemplatesDir + " directory for rendering templates does not exist.\n Configure this by setting htmlRender.TemplatesDir = \"your-tpl-dir/\"")
}
// check for layout file
layoutFile := r.TemplatesDir + r.Layout + r.Ext
if ok, _ := exists(layoutFile); !ok {
panic(layoutFile + " layout file does not exist")
}
}
// getTemplateName returns the name of the template
// For example, if the template path is `templates/articles/list.html`
// getTemplateName would return `articles/list`
func (r *Render) getTemplateName(tpl string) string {
dir, file := filepath.Split(tpl)
dir = strings.Replace(dir, r.TemplatesDir, "", 1)
file = strings.TrimSuffix(file, r.Ext)
return dir + file
}
// exists returns whether the given file or directory exists or not
// http://stackoverflow.com/a/10510783/232619
func exists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return true, err
}
@garenchan
Copy link

garenchan commented Feb 7, 2018

Your custom render cannot work well under windows because of the file path separator!

// getTemplateName returns the name of the template
// For example, if the template path is templates/articles/list.html
// getTemplateName would return articles/list
func (r *Render) getTemplateName(tpl string) string {
dir, file := filepath.Split(tpl)
dir = strings.Replace(dir, r.TemplatesDir, "", 1)
file = strings.TrimSuffix(file, r.Ext)
return dir + file
}

This code is wrong under windows system! And TemplatesDir cannot begin with "./"!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment