From 296cfd9ad083b49fbb34c4a62ddeb63c231cd885 Mon Sep 17 00:00:00 2001 From: Yami Odymel Date: Fri, 29 Aug 2025 14:10:53 +0800 Subject: [PATCH] Add partial render support using "template#block" syntax (fixes gin-gonic/gin#3745) --- README.md | 33 +++++++++++++++++++++++ dynamic.go | 6 +++-- example/partial/example.go | 41 +++++++++++++++++++++++++++++ example/partial/templates/base.html | 2 ++ example/partial/templates/item.html | 1 + multitemplate.go | 14 +++++++++- multitemplate_test.go | 21 +++++++++++++++ tests/partial/base.html | 2 ++ 8 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 example/partial/example.go create mode 100644 example/partial/templates/base.html create mode 100644 example/partial/templates/item.html create mode 100644 tests/partial/base.html diff --git a/README.md b/README.md index a48be38..16a4904 100644 --- a/README.md +++ b/README.md @@ -115,3 +115,36 @@ func loadTemplates(templatesDir string) multitemplate.Renderer { return r } ``` + +### Partial render + +Allows rendering a specific template/block defined in a file with `template#block` syntax. + +See [example/partial/example.go](example/partial/example.go), [htmx ~ Template Fragments](https://htmx.org/essays/template-fragments/) + +```go +func main() { + router := gin.Default() + router.HTMLRender = createMyRender() + + // Route to render full template + router.GET("/", func(c *gin.Context) { + c.HTML(200, "index", gin.H{ + "items": []gin.H{ + {"name": "Apple"}, + {"name": "Banana"}, + {"name": "Cherry"}, + }, + }) + }) + + // Route to render partial template using "index#item" syntax + router.GET("/item", func(c *gin.Context) { + c.HTML(200, "index#item", gin.H{ + "name": "Watermelon", + }) + }) + + router.Run(":8080") +} +``` diff --git a/dynamic.go b/dynamic.go index 113148a..1e1d3b4 100644 --- a/dynamic.go +++ b/dynamic.go @@ -248,12 +248,14 @@ func (r DynamicRender) AddFromFilesFuncsWithOptions( // Instance supply render string func (r DynamicRender) Instance(name string, data interface{}) render.Render { - builder, ok := r[name] + tmplName, partialName := parseTemplateName(name) + builder, ok := r[tmplName] if !ok { - panic(fmt.Sprintf("Dynamic template with name %s not found", name)) + panic(fmt.Sprintf("Dynamic template with name %s not found", tmplName)) } return render.HTML{ Template: builder.buildTemplate(), + Name: partialName, Data: data, } } diff --git a/example/partial/example.go b/example/partial/example.go new file mode 100644 index 0000000..2c54674 --- /dev/null +++ b/example/partial/example.go @@ -0,0 +1,41 @@ +package main + +import ( + "log" + + "github.com/gin-contrib/multitemplate" + "github.com/gin-gonic/gin" +) + +func createMyRender() multitemplate.Renderer { + r := multitemplate.NewRenderer() + r.AddFromFiles("index", "templates/base.html", "templates/item.html") + return r +} + +func main() { + router := gin.Default() + router.HTMLRender = createMyRender() + + // Route to render full template + router.GET("/", func(c *gin.Context) { + c.HTML(200, "index", gin.H{ + "items": []gin.H{ + {"name": "Apple"}, + {"name": "Banana"}, + {"name": "Cherry"}, + }, + }) + }) + + // Route to render partial template using "index#item" syntax + router.GET("/item", func(c *gin.Context) { + c.HTML(200, "index#item", gin.H{ + "name": "Watermelon", + }) + }) + + if err := router.Run(":8080"); err != nil { + log.Fatal(err) + } +} diff --git a/example/partial/templates/base.html b/example/partial/templates/base.html new file mode 100644 index 0000000..4f675bf --- /dev/null +++ b/example/partial/templates/base.html @@ -0,0 +1,2 @@ +

Hello, world

+{{range .items}}{{template "item" .}}{{end}} \ No newline at end of file diff --git a/example/partial/templates/item.html b/example/partial/templates/item.html new file mode 100644 index 0000000..363e618 --- /dev/null +++ b/example/partial/templates/item.html @@ -0,0 +1 @@ +{{define "item"}}
  • {{.name}} is a good fruit.
  • {{end}} \ No newline at end of file diff --git a/multitemplate.go b/multitemplate.go index 79960ae..95357c7 100644 --- a/multitemplate.go +++ b/multitemplate.go @@ -5,6 +5,7 @@ import ( "html/template" "io/fs" "path/filepath" + "strings" "github.com/gin-gonic/gin/render" ) @@ -182,8 +183,19 @@ func (r Render) AddFromFilesFuncsWithOptions( // Instance supply render string func (r Render) Instance(name string, data interface{}) render.Render { + tmplName, partialName := parseTemplateName(name) return render.HTML{ - Template: r[name], + Template: r[tmplName], + Name: partialName, Data: data, } } + +// parseTemplateName parses a template name that may contain a partial reference +// in the format "template#partial" and returns the template name and partial name +func parseTemplateName(name string) (templateName, partialName string) { + if idx := strings.Index(name, "#"); idx > 0 { + return name[:idx], name[idx+1:] + } + return name, "" +} diff --git a/multitemplate_test.go b/multitemplate_test.go index f0d2854..7d853f5 100644 --- a/multitemplate_test.go +++ b/multitemplate_test.go @@ -65,6 +65,13 @@ func createFromFilesWithFuncs() Render { return r } +func createFromPartial() Render { + r := New() + r.AddFromFiles("index", "tests/partial/base.html") + + return r +} + func TestMissingTemplateOrName(t *testing.T) { r := New() tmpl := template.Must(template.New("test").Parse("Welcome to {{ .name }} template")) @@ -168,3 +175,17 @@ func TestDuplicateTemplate(t *testing.T) { r.AddFromString("index", "Welcome to {{ .name }} template") }) } + +func TestPartial(t *testing.T) { + router := gin.New() + router.HTMLRender = createFromPartial() + router.GET("/", func(c *gin.Context) { + c.HTML(200, "index#item", gin.H{ + "name": "apple", + }) + }) + + w := performRequest(router) + assert.Equal(t, 200, w.Code) + assert.Equal(t, "
  • apple
  • ", w.Body.String()) +} diff --git a/tests/partial/base.html b/tests/partial/base.html new file mode 100644 index 0000000..331b73c --- /dev/null +++ b/tests/partial/base.html @@ -0,0 +1,2 @@ +

    Hello, world

    +{{range .items}}{{block "item" .}}
  • {{.name}}
  • {{end}}{{end}} \ No newline at end of file