Skip to content

Commit 542b139

Browse files
a-hCopilot
andauthored
feat: add fragment rendering support (#1216)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent bf0d872 commit 542b139

File tree

14 files changed

+857
-15
lines changed

14 files changed

+857
-15
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Fragments
2+
3+
The `templ.Fragment` component can be used to render a subsection of a template, discarding all other output.
4+
5+
Fragments work well as an optimisation for HTMX, as discussed in https://htmx.org/essays/template-fragments/
6+
7+
## Define fragments
8+
9+
Define a fragment with `@templ.Fragment("name")`, where `"name"` is the identifier for the fragment.
10+
11+
```templ
12+
templ Page() {
13+
<div>Page Header</div>
14+
@templ.Fragment("name") {
15+
<div>Content of the fragment</div>
16+
}
17+
}
18+
```
19+
20+
To avoid name clashes with other libraries, you can define a custom type for your package.
21+
22+
```templ
23+
type nameFragmentKey struct {}
24+
var Name = nameFragmentKey{}
25+
26+
templ Page() {
27+
<div>Page Header</div>
28+
@templ.Fragment(Name) {
29+
<div>Content of the fragment</div>
30+
}
31+
}
32+
```
33+
34+
## Use with HTTP
35+
36+
The most common use case for `Fragment` is to render only a specific part of the template to the HTML response, while discarding the rest of the output.
37+
38+
To render only the "name" fragment from the `Page` template, use the `templ.WithFragments("name")` option when creating the HTTP handler:
39+
40+
```go title="main.go"
41+
handler := templ.Handler(Page(), templ.WithFragments("name"))
42+
http.Handle("/", handler)
43+
```
44+
45+
When the HTTP request is made, only the content of the specified fragment will be returned in the response:
46+
47+
```html title="output.html"
48+
<div>Content of the fragment</div>
49+
```
50+
51+
:::note
52+
The whole of the template is rendered, so any function calls or logic in the template will still be executed, but only the specified fragment's output is sent to the client.
53+
:::
54+
55+
If the `templ.WithFragments("name")` option is omitted, the whole page is rendered as normal.
56+
57+
```go title="main.go"
58+
handler := templ.Handler(Page())
59+
http.Handle("/", handler)
60+
```
61+
62+
```html title="output.html"
63+
<div>Page Header</div>
64+
<div>Content of the fragment</div>
65+
```
66+
67+
## Use outside of an HTTP handler
68+
69+
To use outside of an HTTP handler, e.g. when generating static content, you can render fragments with the `templ.RenderFragments` function.
70+
71+
```go
72+
w := new(bytes.Buffer)
73+
if err := templ.RenderFragments(context.Background(), w, fragmentPage, "name"); err != nil {
74+
t.Fatalf("failed to render: %v", err)
75+
}
76+
77+
// <div>Content of the fragment</div>
78+
html := w.String()
79+
```
80+
81+
:::note
82+
All fragments with matching identifiers will be rendered. If the fragment identifier isn't matched, no output will be produced.
83+
:::
84+
85+
## Nested fragments
86+
87+
Fragments can be nested, allowing for complex structures to be defined and rendered selectively.
88+
89+
Given this example templ file:
90+
91+
```templ
92+
templ Page() {
93+
@templ.Fragment("outer") {
94+
<div>Outer Fragment Start</div>
95+
@templ.Fragment("inner") {
96+
<div>Inner Fragment Content</div>
97+
}
98+
<div>Outer Fragment End</div>
99+
}
100+
}
101+
```
102+
103+
If the `outer` fragment is selected for rendering, then the `inner` fragment is also rendered.
104+
105+
## HTMX example
106+
107+
```templ title="main.templ"
108+
package main
109+
110+
import (
111+
"fmt"
112+
"net/http"
113+
"strconv"
114+
)
115+
116+
type PageState struct {
117+
Counter int
118+
Next int
119+
}
120+
121+
templ Page(state PageState) {
122+
<html>
123+
<head>
124+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
125+
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.3/dist/missing.min.css"/>
126+
</head>
127+
<body>
128+
@templ.Fragment("buttonOnly") {
129+
<button hx-get={ fmt.Sprintf("/?counter=%d&template=buttonOnly", state.Next) } hx-swap="outerHTML">
130+
This Button Has Been Clicked { state.Counter } Times
131+
</button>
132+
}
133+
</body>
134+
</html>
135+
}
136+
137+
// handleRequest does the work to execute the template (or fragment) and serve the result.
138+
// It's mostly boilerplate, so don't get hung up on it.
139+
func handleRequest(w http.ResponseWriter, r *http.Request) {
140+
// Collect state info to pass to the template.
141+
var state PageState
142+
state.Counter, _ = strconv.Atoi(r.URL.Query().Get("counter"))
143+
state.Next = state.Counter + 1
144+
145+
// If the template querystring paramater is set, render the pecific fragment.
146+
var opts []func(*templ.ComponentHandler)
147+
if templateName := r.URL.Query().Get("template"); templateName != "" {
148+
opts = append(opts, templ.WithFragments(templateName))
149+
}
150+
151+
// Render the template or fragment and serve it.
152+
templ.Handler(Page(state), opts...).ServeHTTP(w, r)
153+
}
154+
155+
func main() {
156+
// Handle the template.
157+
http.HandleFunc("/", handleRequest)
158+
159+
// Start the server.
160+
fmt.Println("Server is running at http://localhost:8080")
161+
http.ListenAndServe("localhost:8080", nil)
162+
}
163+
```
164+
165+
:::note
166+
This was adapted from `benpate`'s Go stdlib example at https://gist.github.com/benpate/f92b77ea9b3a8503541eb4b9eb515d8a
167+
:::

examples/htmx-fragments/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
## Tasks
2+
3+
### run
4+
5+
```bash
6+
templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
7+
```

examples/htmx-fragments/go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/a-h/templ/examples/htmx-fragments
2+
3+
go 1.23.0
4+
5+
toolchain go1.23.6
6+
7+
require github.com/a-h/templ v0.2.747
8+
9+
replace github.com/a-h/templ => ../../

examples/htmx-fragments/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
2+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

examples/htmx-fragments/main.templ

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strconv"
7+
)
8+
9+
type PageState struct {
10+
Counter int
11+
Next int
12+
}
13+
14+
templ Page(state PageState) {
15+
<!DOCTYPE html>
16+
<html>
17+
<head>
18+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
19+
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.3/dist/missing.min.css"/>
20+
<title>Template Fragment Example</title>
21+
</head>
22+
<body>
23+
<h1>Template Fragment Example</h1>
24+
<p>
25+
This page demonstrates how to create and serve
26+
<a href="https://htmx.org/essays/template-fragments/">template fragments</a>
27+
using <a href="https://templ.guide">templ</a> in Go.
28+
</p>
29+
<p>
30+
This is accomplished by using the "templ.Fragment" component, which lets you
31+
select areas to include in the output.
32+
</p>
33+
<p>
34+
Adapted from https://gist.github.com/benpate/f92b77ea9b3a8503541eb4b9eb515d8a
35+
</p>
36+
<!-- Here's the fragment. We can target it by executing the "buttonOnly" template. -->
37+
@templ.Fragment("buttonOnly") {
38+
<button hx-get={ fmt.Sprintf("/?counter=%d&template=buttonOnly", state.Next) } hx-swap="outerHTML">
39+
This Button Has Been Clicked { state.Counter } Times
40+
</button>
41+
}
42+
</body>
43+
</html>
44+
}
45+
46+
// handleRequest does the work to execute the template (or fragment) and serve the result.
47+
// It's mostly boilerplate, so don't get hung up on it.
48+
func handleRequest(w http.ResponseWriter, r *http.Request) {
49+
// Collect state info to pass to the template.
50+
var state PageState
51+
state.Counter, _ = strconv.Atoi(r.URL.Query().Get("counter"))
52+
state.Next = state.Counter + 1
53+
54+
// If the template querystring paramater is set, render the pecific fragment.
55+
var opts []func(*templ.ComponentHandler)
56+
if templateName := r.URL.Query().Get("template"); templateName != "" {
57+
opts = append(opts, templ.WithFragments(templateName))
58+
}
59+
60+
// Render the template or fragment and serve it.
61+
templ.Handler(Page(state), opts...).ServeHTTP(w, r)
62+
}
63+
64+
func main() {
65+
// Handle the template.
66+
http.HandleFunc("/", handleRequest)
67+
68+
// Start the server.
69+
fmt.Println("Server is running at http://localhost:8080")
70+
http.ListenAndServe("localhost:8080", nil)
71+
}

examples/htmx-fragments/main_templ.go

Lines changed: 129 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)