Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
```
6 changes: 4 additions & 2 deletions dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
41 changes: 41 additions & 0 deletions example/partial/example.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 2 additions & 0 deletions example/partial/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p>Hello, world</p>
{{range .items}}{{template "item" .}}{{end}}
1 change: 1 addition & 0 deletions example/partial/templates/item.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{define "item"}}<li>{{.name}} is a good fruit.</li>{{end}}
14 changes: 13 additions & 1 deletion multitemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"html/template"
"io/fs"
"path/filepath"
"strings"

"github.com/gin-gonic/gin/render"
)
Expand Down Expand Up @@ -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 {
Copy link

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition idx > 0 excludes template names that start with '#'. This should be idx >= 0 to handle cases where the partial name starts immediately with '#', or idx > 0 if you specifically want to require a template name before the '#'.

Suggested change
if idx := strings.Index(name, "#"); idx > 0 {
if idx := strings.Index(name, "#"); idx >= 0 {

Copilot uses AI. Check for mistakes.
return name[:idx], name[idx+1:]
}
return name, ""
}
21 changes: 21 additions & 0 deletions multitemplate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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, "<li>apple</li>", w.Body.String())
}
2 changes: 2 additions & 0 deletions tests/partial/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<p>Hello, world</p>
{{range .items}}{{block "item" .}}<li>{{.name}}</li>{{end}}{{end}}
Loading