Skip to content

Commit fb44b3e

Browse files
a-hjoerdav
andauthored
feat: add new JS handling features - templ.JSFuncCall and templ.JSUnsafeFuncCall (#1038)
Co-authored-by: Joe Davidson <joe.davidson.21111@gmail.com>
1 parent fcc0519 commit fb44b3e

File tree

16 files changed

+712
-74
lines changed

16 files changed

+712
-74
lines changed

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.819
1+
0.3.822

docs/docs/03-syntax-and-usage/12-script-templates.md

Lines changed: 142 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,149 @@ templ body() {
1515
}
1616
```
1717

18-
To pass data from the server to client-side scripts, see [Passing server-side data to scripts](#passing-server-side-data-to-scripts).
18+
:::tip
19+
To ensure that a `<script>` tag within a templ component is only rendered once per HTTP response (or context), use a [templ.OnceHandle](18-render-once.md).
1920

20-
## Adding client side behaviours to components
21+
Using a `templ.OnceHandle` allows a component to define global client-side scripts that it needs to run without including the scripts multiple times in the response.
22+
:::
2123

22-
To ensure that a `<script>` tag within a templ component is only rendered once per HTTP response, use a [templ.OnceHandle](18-render-once.md).
24+
## Pass Go data to JavaScript
2325

24-
Using a `templ.OnceHandle` allows a component to define global client-side scripts that it needs to run without including the scripts multiple times in the response.
26+
### Pass Go data to a JavaScript event handler
27+
28+
Use `templ.JSFuncCall` to pass server-side data to client-side scripts by calling a JavaScript function.
29+
30+
```templ title="input.templ"
31+
templ Component(data CustomType) {
32+
<button onclick={ templ.JSFuncCall("alert", data.Message) }>Show alert</button>
33+
}
34+
```
35+
36+
The data passed to the `alert` function is JSON encoded, so if `data.Message` was the string value of `Hello, from the JSFuncCall data`, the output would be:
37+
38+
```html title="output.html"
39+
<button onclick="alert('Hello, from the JSFuncCall data')">Show alert</button>
40+
```
41+
42+
### Pass event objects to an Event Handler
43+
44+
HTML element `on*` attributes pass an event object to the function. To pass the event object to a function, use `templ.JSExpression`.
45+
46+
47+
:::warning
48+
`templ.JSExpression` bypasses JSON encoding, so the string value is output directly to the HTML - this can be a security risk if the data is not trusted, e.g. the data is user input, not a compile-time constant.
49+
:::
50+
51+
```templ title="input.templ"
52+
<script type="text/javascript">
53+
function clickHandler(event, message) {
54+
alert(message);
55+
event.preventDefault();
56+
}
57+
</script>
58+
<button onclick={ templ.JSFuncCall("clickHandler", templ.JSExpression("event"), "message from Go") }>Show event</button>
59+
```
60+
61+
The output would be:
62+
63+
```html title="output.html"
64+
<script type="text/javascript">
65+
function clickHandler(event, message) {
66+
alert(message);
67+
event.preventDefault();
68+
}
69+
</script>
70+
<button onclick="clickHandler(event, 'message from Go')">Show event</button>
71+
```
72+
73+
### Call client side functions with server side data
74+
75+
Use `templ.JSFuncCall` to call a client-side function with server-side data.
76+
77+
`templ.JSFuncCall` takes a function name and a variadic list of arguments. The arguments are JSON encoded and passed to the function.
78+
79+
In the case that the function name is invalid (e.g. contains `</script>` or is a JavaScript expression, not a function name), the function name will be sanitized to `__templ_invalid_function_name`.
80+
81+
```templ title="components.templ"
82+
templ InitializeClientSideScripts(data CustomType) {
83+
@templ.JSFuncCall("functionToCall", data.Name, data.Age)
84+
}
85+
```
86+
87+
This will output a `<script>` tag that calls the `functionToCall` function with the `Name` and `Age` properties of the `data` object.
88+
89+
```html title="output.html"
90+
<script type="text/javascript">
91+
functionToCall("John", 42);
92+
</script>
93+
```
94+
95+
:::tip
96+
If you want to write out an arbitrary string containing JavaScript, and are sure it is safe, you can use `templ.JSUnsafeFuncCall` to bypass script sanitization.
97+
98+
Whatever string you pass to `templ.JSUnsafeFuncCall` will be output directly to the HTML, so be sure to validate the input.
99+
:::
100+
101+
### Pass server-side data to the client in a HTML attribute
102+
103+
A common approach used by libraries like alpine.js is to pass data to the client in a HTML attribute.
104+
105+
To pass server-side data to the client in a HTML attribute, use `templ.JSONString` to encode the data as a JSON string.
106+
107+
```templ title="input.templ"
108+
templ body(data any) {
109+
<button id="alerter" alert-data={ templ.JSONString(data) }>Show alert</button>
110+
}
111+
```
112+
113+
```html title="output.html"
114+
<button id="alerter" alert-data="{&quot;msg&quot;:&quot;Hello, from the attribute data&quot;}">Show alert</button>
115+
```
116+
117+
The data in the attribute can then be accessed from client-side JavaScript.
118+
119+
```javascript
120+
const button = document.getElementById('alerter');
121+
const data = JSON.parse(button.getAttribute('alert-data'));
122+
```
123+
124+
[alpine.js](https://alpinejs.dev/) uses `x-*` attributes to pass data to the client:
125+
126+
```templ
127+
templ DataDisplay(data DataType) {
128+
<div x-data={ templ.JSONString(data) }>
129+
...
130+
</div>
131+
}
132+
```
25133

26-
The example below also demonstrates applying behaviour that's defined in a multiline script to its sibling element.
134+
### Pass server-side data to the client in a script element
135+
136+
In addition to passing data in HTML attributes, you can also pass data to the client in a `<script>` element.
137+
138+
```templ title="input.templ"
139+
templ body(data any) {
140+
@templ.JSONScript("id", data)
141+
}
142+
```
143+
144+
```html title="output.html"
145+
<script id="id" type="application/json">{"msg":"Hello, from the script data"}</script>
146+
```
147+
148+
The data in the script tag can then be accessed from client-side JavaScript.
149+
150+
```javascript
151+
const data = JSON.parse(document.getElementById('id').textContent);
152+
```
153+
154+
## Avoiding inline event handlers
155+
156+
According to Mozilla, [inline event handlers are considered bad practice](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Events#inline_event_handlers_%E2%80%94_dont_use_these).
157+
158+
This example demonstrates how to add client-side behaviour to a component using a script tag.
159+
160+
The example uses a `templ.OnceHandle` to define global client-side scripts that are required, without rendering the scripts multiple times in the response.
27161

28162
```templ title="component.templ"
29163
package main
@@ -147,47 +281,6 @@ http.ListenAndServe("localhost:8080", mux)
147281
```
148282
:::
149283

150-
## Passing server-side data to scripts
151-
152-
Pass data from the server to the client by embedding it in the HTML as a JSON object in an attribute or script tag.
153-
154-
### Pass server-side data to the client in a HTML attribute
155-
156-
```templ title="input.templ"
157-
templ body(data any) {
158-
<button id="alerter" alert-data={ templ.JSONString(data) }>Show alert</button>
159-
}
160-
```
161-
162-
```html title="output.html"
163-
<button id="alerter" alert-data="{&quot;msg&quot;:&quot;Hello, from the attribute data&quot;}">Show alert</button>
164-
```
165-
166-
The data in the attribute can then be accessed from client-side JavaScript.
167-
168-
```javascript
169-
const button = document.getElementById('alerter');
170-
const data = JSON.parse(button.getAttribute('alert-data'));
171-
```
172-
173-
### Pass server-side data to the client in a script element
174-
175-
```templ title="input.templ"
176-
templ body(data any) {
177-
@templ.JSONScript("id", data)
178-
}
179-
```
180-
181-
```html title="output.html"
182-
<script id="id" type="application/json">{"msg":"Hello, from the script data"}</script>
183-
```
184-
185-
The data in the script tag can then be accessed from client-side JavaScript.
186-
187-
```javascript
188-
const data = JSON.parse(document.getElementById('id').textContent);
189-
```
190-
191284
## Working with NPM projects
192285

193286
https://github.com/a-h/templ/tree/main/examples/typescript contains a TypeScript example that uses `esbuild` to transpile TypeScript into plain JavaScript, along with any required `npm` modules.
@@ -272,7 +365,9 @@ func main() {
272365
## Script templates
273366

274367
:::warning
275-
Script templates are a legacy feature and are not recommended for new projects. Use standard `<script>` tags to import a standalone JavaScript file, optionally created by a bundler like `esbuild`.
368+
Script templates are a legacy feature and are not recommended for new projects.
369+
370+
Use the `templ.JSFuncCall`, `templ.JSONString` and other features of templ alongside standard `<script>` tags to import standalone JavaScript files, optionally created by a bundler like `esbuild`.
276371
:::
277372

278373
If you need to pass Go data to scripts, you can use a script template.

generator/htmldiff/diff.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ func DiffStrings(expected, actual string) (diff string, err error) {
4949
}
5050

5151
func Diff(input templ.Component, expected string) (diff string, err error) {
52-
return DiffCtx(context.Background(), input, expected)
52+
_, diff, err = DiffCtx(context.Background(), input, expected)
53+
return diff, err
5354
}
5455

55-
func DiffCtx(ctx context.Context, input templ.Component, expected string) (diff string, err error) {
56+
func DiffCtx(ctx context.Context, input templ.Component, expected string) (formattedInput, diff string, err error) {
5657
var wg sync.WaitGroup
5758
wg.Add(2)
5859

@@ -90,5 +91,5 @@ func DiffCtx(ctx context.Context, input templ.Component, expected string) (diff
9091
// Wait for processing.
9192
wg.Wait()
9293

93-
return cmp.Diff(expected, actual.String()), errors.Join(errs...)
94+
return actual.String(), cmp.Diff(expected, actual.String()), errors.Join(errs...)
9495
}

generator/test-context/render_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func Test(t *testing.T) {
1616

1717
ctx := context.WithValue(context.Background(), contextKeyName, "test")
1818

19-
diff, err := htmldiff.DiffCtx(ctx, component, expected)
19+
_, diff, err := htmldiff.DiffCtx(ctx, component, expected)
2020
if err != nil {
2121
t.Fatal(err)
2222
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<button onClick="anythingILike('blah')">
2+
Click me
3+
</button>
4+
<script type="text/javascript">
5+
// Arbitrary JS code
6+
</script>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package testjsunsafeusage
2+
3+
import (
4+
_ "embed"
5+
"testing"
6+
7+
"github.com/a-h/templ/generator/htmldiff"
8+
)
9+
10+
//go:embed expected.html
11+
var expected string
12+
13+
func Test(t *testing.T) {
14+
component := TestComponent()
15+
16+
diff, err := htmldiff.Diff(component, expected)
17+
if err != nil {
18+
t.Fatal(err)
19+
}
20+
if diff != "" {
21+
t.Error(diff)
22+
}
23+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package testjsunsafeusage
2+
3+
templ TestComponent() {
4+
<button onClick={ templ.JSUnsafeFuncCall("anythingILike('blah')") }>Click me</button>
5+
@templ.JSUnsafeFuncCall("// Arbitrary JS code")
6+
}

generator/test-js-unsafe-usage/template_templ.go

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<button onclick="alert(&#34;Hello, World!&#34;)">
2+
Click me
3+
</button>
4+
<script type="text/javascript">
5+
function customAlert(msg, date) {
6+
alert(msg + " " + date);
7+
}
8+
</script>
9+
<button onclick="customAlert(&#34;Hello, custom alert 1: &#34;,&#34;2020-01-01T00:00:00Z&#34;)">
10+
Click me
11+
</button>
12+
<button onclick="customAlert(&#34;Hello, custom alert 2: &#34;,&#34;2020-01-01T00:00:00Z&#34;)">
13+
Click me
14+
</button>
15+
<script type="text/javascript">
16+
customAlert("Runs on page load","2020-01-01T00:00:00Z")
17+
</script>
18+
<script>
19+
function onClickEventHandler(event, data) {
20+
alert(event.type);
21+
alert(data)
22+
event.preventDefault();
23+
}
24+
</script>
25+
<button onclick="onClickEventHandler(event,&#34;1234&#34;)">
26+
Pass event handler
27+
</button>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package testjsusage
2+
3+
import (
4+
_ "embed"
5+
"testing"
6+
7+
"github.com/a-h/templ/generator/htmldiff"
8+
)
9+
10+
//go:embed expected.html
11+
var expected string
12+
13+
func Test(t *testing.T) {
14+
component := TestComponent()
15+
16+
diff, err := htmldiff.Diff(component, expected)
17+
if err != nil {
18+
t.Fatal(err)
19+
}
20+
if diff != "" {
21+
t.Error(diff)
22+
}
23+
}

0 commit comments

Comments
 (0)