mirror of
https://github.com/a-h/templ.git
synced 2025-02-06 10:03:16 +00:00
feat: add new JS handling features - templ.JSFuncCall and templ.JSUnsafeFuncCall (#1038)
Co-authored-by: Joe Davidson <joe.davidson.21111@gmail.com>
This commit is contained in:
parent
fcc0519ac7
commit
fb44b3e841
@ -15,15 +15,149 @@ templ body() {
|
||||
}
|
||||
```
|
||||
|
||||
To pass data from the server to client-side scripts, see [Passing server-side data to scripts](#passing-server-side-data-to-scripts).
|
||||
|
||||
## Adding client side behaviours to components
|
||||
|
||||
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).
|
||||
:::tip
|
||||
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).
|
||||
|
||||
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.
|
||||
:::
|
||||
|
||||
The example below also demonstrates applying behaviour that's defined in a multiline script to its sibling element.
|
||||
## Pass Go data to JavaScript
|
||||
|
||||
### Pass Go data to a JavaScript event handler
|
||||
|
||||
Use `templ.JSFuncCall` to pass server-side data to client-side scripts by calling a JavaScript function.
|
||||
|
||||
```templ title="input.templ"
|
||||
templ Component(data CustomType) {
|
||||
<button onclick={ templ.JSFuncCall("alert", data.Message) }>Show alert</button>
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```html title="output.html"
|
||||
<button onclick="alert('Hello, from the JSFuncCall data')">Show alert</button>
|
||||
```
|
||||
|
||||
### Pass event objects to an Event Handler
|
||||
|
||||
HTML element `on*` attributes pass an event object to the function. To pass the event object to a function, use `templ.JSExpression`.
|
||||
|
||||
|
||||
:::warning
|
||||
`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.
|
||||
:::
|
||||
|
||||
```templ title="input.templ"
|
||||
<script type="text/javascript">
|
||||
function clickHandler(event, message) {
|
||||
alert(message);
|
||||
event.preventDefault();
|
||||
}
|
||||
</script>
|
||||
<button onclick={ templ.JSFuncCall("clickHandler", templ.JSExpression("event"), "message from Go") }>Show event</button>
|
||||
```
|
||||
|
||||
The output would be:
|
||||
|
||||
```html title="output.html"
|
||||
<script type="text/javascript">
|
||||
function clickHandler(event, message) {
|
||||
alert(message);
|
||||
event.preventDefault();
|
||||
}
|
||||
</script>
|
||||
<button onclick="clickHandler(event, 'message from Go')">Show event</button>
|
||||
```
|
||||
|
||||
### Call client side functions with server side data
|
||||
|
||||
Use `templ.JSFuncCall` to call a client-side function with server-side data.
|
||||
|
||||
`templ.JSFuncCall` takes a function name and a variadic list of arguments. The arguments are JSON encoded and passed to the function.
|
||||
|
||||
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`.
|
||||
|
||||
```templ title="components.templ"
|
||||
templ InitializeClientSideScripts(data CustomType) {
|
||||
@templ.JSFuncCall("functionToCall", data.Name, data.Age)
|
||||
}
|
||||
```
|
||||
|
||||
This will output a `<script>` tag that calls the `functionToCall` function with the `Name` and `Age` properties of the `data` object.
|
||||
|
||||
```html title="output.html"
|
||||
<script type="text/javascript">
|
||||
functionToCall("John", 42);
|
||||
</script>
|
||||
```
|
||||
|
||||
:::tip
|
||||
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.
|
||||
|
||||
Whatever string you pass to `templ.JSUnsafeFuncCall` will be output directly to the HTML, so be sure to validate the input.
|
||||
:::
|
||||
|
||||
### Pass server-side data to the client in a HTML attribute
|
||||
|
||||
A common approach used by libraries like alpine.js is to pass data to the client in a HTML attribute.
|
||||
|
||||
To pass server-side data to the client in a HTML attribute, use `templ.JSONString` to encode the data as a JSON string.
|
||||
|
||||
```templ title="input.templ"
|
||||
templ body(data any) {
|
||||
<button id="alerter" alert-data={ templ.JSONString(data) }>Show alert</button>
|
||||
}
|
||||
```
|
||||
|
||||
```html title="output.html"
|
||||
<button id="alerter" alert-data="{"msg":"Hello, from the attribute data"}">Show alert</button>
|
||||
```
|
||||
|
||||
The data in the attribute can then be accessed from client-side JavaScript.
|
||||
|
||||
```javascript
|
||||
const button = document.getElementById('alerter');
|
||||
const data = JSON.parse(button.getAttribute('alert-data'));
|
||||
```
|
||||
|
||||
[alpine.js](https://alpinejs.dev/) uses `x-*` attributes to pass data to the client:
|
||||
|
||||
```templ
|
||||
templ DataDisplay(data DataType) {
|
||||
<div x-data={ templ.JSONString(data) }>
|
||||
...
|
||||
</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Pass server-side data to the client in a script element
|
||||
|
||||
In addition to passing data in HTML attributes, you can also pass data to the client in a `<script>` element.
|
||||
|
||||
```templ title="input.templ"
|
||||
templ body(data any) {
|
||||
@templ.JSONScript("id", data)
|
||||
}
|
||||
```
|
||||
|
||||
```html title="output.html"
|
||||
<script id="id" type="application/json">{"msg":"Hello, from the script data"}</script>
|
||||
```
|
||||
|
||||
The data in the script tag can then be accessed from client-side JavaScript.
|
||||
|
||||
```javascript
|
||||
const data = JSON.parse(document.getElementById('id').textContent);
|
||||
```
|
||||
|
||||
## Avoiding inline event handlers
|
||||
|
||||
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).
|
||||
|
||||
This example demonstrates how to add client-side behaviour to a component using a script tag.
|
||||
|
||||
The example uses a `templ.OnceHandle` to define global client-side scripts that are required, without rendering the scripts multiple times in the response.
|
||||
|
||||
```templ title="component.templ"
|
||||
package main
|
||||
@ -147,47 +281,6 @@ http.ListenAndServe("localhost:8080", mux)
|
||||
```
|
||||
:::
|
||||
|
||||
## Passing server-side data to scripts
|
||||
|
||||
Pass data from the server to the client by embedding it in the HTML as a JSON object in an attribute or script tag.
|
||||
|
||||
### Pass server-side data to the client in a HTML attribute
|
||||
|
||||
```templ title="input.templ"
|
||||
templ body(data any) {
|
||||
<button id="alerter" alert-data={ templ.JSONString(data) }>Show alert</button>
|
||||
}
|
||||
```
|
||||
|
||||
```html title="output.html"
|
||||
<button id="alerter" alert-data="{"msg":"Hello, from the attribute data"}">Show alert</button>
|
||||
```
|
||||
|
||||
The data in the attribute can then be accessed from client-side JavaScript.
|
||||
|
||||
```javascript
|
||||
const button = document.getElementById('alerter');
|
||||
const data = JSON.parse(button.getAttribute('alert-data'));
|
||||
```
|
||||
|
||||
### Pass server-side data to the client in a script element
|
||||
|
||||
```templ title="input.templ"
|
||||
templ body(data any) {
|
||||
@templ.JSONScript("id", data)
|
||||
}
|
||||
```
|
||||
|
||||
```html title="output.html"
|
||||
<script id="id" type="application/json">{"msg":"Hello, from the script data"}</script>
|
||||
```
|
||||
|
||||
The data in the script tag can then be accessed from client-side JavaScript.
|
||||
|
||||
```javascript
|
||||
const data = JSON.parse(document.getElementById('id').textContent);
|
||||
```
|
||||
|
||||
## Working with NPM projects
|
||||
|
||||
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() {
|
||||
## Script templates
|
||||
|
||||
:::warning
|
||||
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`.
|
||||
Script templates are a legacy feature and are not recommended for new projects.
|
||||
|
||||
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`.
|
||||
:::
|
||||
|
||||
If you need to pass Go data to scripts, you can use a script template.
|
||||
|
@ -49,10 +49,11 @@ func DiffStrings(expected, actual string) (diff string, err error) {
|
||||
}
|
||||
|
||||
func Diff(input templ.Component, expected string) (diff string, err error) {
|
||||
return DiffCtx(context.Background(), input, expected)
|
||||
_, diff, err = DiffCtx(context.Background(), input, expected)
|
||||
return diff, err
|
||||
}
|
||||
|
||||
func DiffCtx(ctx context.Context, input templ.Component, expected string) (diff string, err error) {
|
||||
func DiffCtx(ctx context.Context, input templ.Component, expected string) (formattedInput, diff string, err error) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
@ -90,5 +91,5 @@ func DiffCtx(ctx context.Context, input templ.Component, expected string) (diff
|
||||
// Wait for processing.
|
||||
wg.Wait()
|
||||
|
||||
return cmp.Diff(expected, actual.String()), errors.Join(errs...)
|
||||
return actual.String(), cmp.Diff(expected, actual.String()), errors.Join(errs...)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ func Test(t *testing.T) {
|
||||
|
||||
ctx := context.WithValue(context.Background(), contextKeyName, "test")
|
||||
|
||||
diff, err := htmldiff.DiffCtx(ctx, component, expected)
|
||||
_, diff, err := htmldiff.DiffCtx(ctx, component, expected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
6
generator/test-js-unsafe-usage/expected.html
Normal file
6
generator/test-js-unsafe-usage/expected.html
Normal file
@ -0,0 +1,6 @@
|
||||
<button onClick="anythingILike('blah')">
|
||||
Click me
|
||||
</button>
|
||||
<script type="text/javascript">
|
||||
// Arbitrary JS code
|
||||
</script>
|
23
generator/test-js-unsafe-usage/render_test.go
Normal file
23
generator/test-js-unsafe-usage/render_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package testjsunsafeusage
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/templ/generator/htmldiff"
|
||||
)
|
||||
|
||||
//go:embed expected.html
|
||||
var expected string
|
||||
|
||||
func Test(t *testing.T) {
|
||||
component := TestComponent()
|
||||
|
||||
diff, err := htmldiff.Diff(component, expected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
6
generator/test-js-unsafe-usage/template.templ
Normal file
6
generator/test-js-unsafe-usage/template.templ
Normal file
@ -0,0 +1,6 @@
|
||||
package testjsunsafeusage
|
||||
|
||||
templ TestComponent() {
|
||||
<button onClick={ templ.JSUnsafeFuncCall("anythingILike('blah')") }>Click me</button>
|
||||
@templ.JSUnsafeFuncCall("// Arbitrary JS code")
|
||||
}
|
56
generator/test-js-unsafe-usage/template_templ.go
Normal file
56
generator/test-js-unsafe-usage/template_templ.go
Normal file
@ -0,0 +1,56 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
package testjsunsafeusage
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func TestComponent() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSUnsafeFuncCall("anythingILike('blah')"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button onClick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.ComponentScript = templ.JSUnsafeFuncCall("anythingILike('blah')")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">Click me</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.JSUnsafeFuncCall("// Arbitrary JS code").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
27
generator/test-js-usage/expected.html
Normal file
27
generator/test-js-usage/expected.html
Normal file
@ -0,0 +1,27 @@
|
||||
<button onclick="alert("Hello, World!")">
|
||||
Click me
|
||||
</button>
|
||||
<script type="text/javascript">
|
||||
function customAlert(msg, date) {
|
||||
alert(msg + " " + date);
|
||||
}
|
||||
</script>
|
||||
<button onclick="customAlert("Hello, custom alert 1: ","2020-01-01T00:00:00Z")">
|
||||
Click me
|
||||
</button>
|
||||
<button onclick="customAlert("Hello, custom alert 2: ","2020-01-01T00:00:00Z")">
|
||||
Click me
|
||||
</button>
|
||||
<script type="text/javascript">
|
||||
customAlert("Runs on page load","2020-01-01T00:00:00Z")
|
||||
</script>
|
||||
<script>
|
||||
function onClickEventHandler(event, data) {
|
||||
alert(event.type);
|
||||
alert(data)
|
||||
event.preventDefault();
|
||||
}
|
||||
</script>
|
||||
<button onclick="onClickEventHandler(event,"1234")">
|
||||
Pass event handler
|
||||
</button>
|
23
generator/test-js-usage/render_test.go
Normal file
23
generator/test-js-usage/render_test.go
Normal file
@ -0,0 +1,23 @@
|
||||
package testjsusage
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"testing"
|
||||
|
||||
"github.com/a-h/templ/generator/htmldiff"
|
||||
)
|
||||
|
||||
//go:embed expected.html
|
||||
var expected string
|
||||
|
||||
func Test(t *testing.T) {
|
||||
component := TestComponent()
|
||||
|
||||
diff, err := htmldiff.Diff(component, expected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
27
generator/test-js-usage/template.templ
Normal file
27
generator/test-js-usage/template.templ
Normal file
@ -0,0 +1,27 @@
|
||||
package testjsusage
|
||||
|
||||
import "time"
|
||||
|
||||
var onceHandle = templ.NewOnceHandle()
|
||||
|
||||
templ TestComponent() {
|
||||
<button onClick={ templ.JSFuncCall("alert", "Hello, World!") }>Click me</button>
|
||||
@onceHandle.Once() {
|
||||
<script type="text/javascript">
|
||||
function customAlert(msg, date) {
|
||||
alert(msg + " " + date);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
<button onClick={ templ.JSFuncCall("customAlert", "Hello, custom alert 1: ", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) }>Click me</button>
|
||||
<button onClick={ templ.JSFuncCall("customAlert", "Hello, custom alert 2: ", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) }>Click me</button>
|
||||
@templ.JSFuncCall("customAlert", "Runs on page load", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
<script>
|
||||
function onClickEventHandler(event, data) {
|
||||
alert(event.type);
|
||||
alert(data)
|
||||
event.preventDefault();
|
||||
}
|
||||
</script>
|
||||
<button onclick={ templ.JSFuncCall("onClickEventHandler", templ.JSExpression("event"), "1234") }>Pass event handler</button>
|
||||
}
|
137
generator/test-js-usage/template_templ.go
Normal file
137
generator/test-js-usage/template_templ.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
package testjsusage
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "time"
|
||||
|
||||
var onceHandle = templ.NewOnceHandle()
|
||||
|
||||
func TestComponent() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("alert", "Hello, World!"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button onClick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 templ.ComponentScript = templ.JSFuncCall("alert", "Hello, World!")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">Click me</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<script type=\"text/javascript\">\n\t\t\tfunction customAlert(msg, date) {\n\t\t\t\talert(msg + \" \" + date);\n\t\t\t}\n\t\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = onceHandle.Once().Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("customAlert", "Hello, custom alert 1: ", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<button onClick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.ComponentScript = templ.JSFuncCall("customAlert", "Hello, custom alert 1: ", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\">Click me</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("customAlert", "Hello, custom alert 2: ", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<button onClick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 templ.ComponentScript = templ.JSFuncCall("customAlert", "Hello, custom alert 2: ", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">Click me</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.JSFuncCall("customAlert", "Runs on page load", time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<script>\n\t\tfunction onClickEventHandler(event, data) {\n\t\t\talert(event.type);\n\t\t\talert(data)\n\t\t\tevent.preventDefault();\n\t\t}\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.JSFuncCall("onClickEventHandler", templ.JSExpression("event"), "1234"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<button onclick=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 templ.ComponentScript = templ.JSFuncCall("onClickEventHandler", templ.JSExpression("event"), "1234")
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6.Call)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">Pass event handler</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
@ -16,7 +16,7 @@ func Test(t *testing.T) {
|
||||
component := ThreeButtons()
|
||||
|
||||
ctx := templ.WithNonce(context.Background(), "nonce1")
|
||||
diff, err := htmldiff.DiffCtx(ctx, component, expected)
|
||||
_, diff, err := htmldiff.DiffCtx(ctx, component, expected)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
40
js.go
Normal file
40
js.go
Normal file
@ -0,0 +1,40 @@
|
||||
package templ
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"html"
|
||||
)
|
||||
|
||||
// JSUnsafeFuncCall calls arbitrary JavaScript in the js parameter.
|
||||
//
|
||||
// Use of this function presents a security risk - the JavaScript must come
|
||||
// from a trusted source, because it will be included as-is in the output.
|
||||
func JSUnsafeFuncCall[T ~string](js T) ComponentScript {
|
||||
sum := sha256.Sum256([]byte(js))
|
||||
return ComponentScript{
|
||||
Name: "jsUnsafeFuncCall_" + hex.EncodeToString(sum[:]),
|
||||
// Function is empty because the body of the function is defined elsewhere,
|
||||
// e.g. in a <script> tag within a templ.Once block.
|
||||
Function: "",
|
||||
Call: html.EscapeString(string(js)),
|
||||
CallInline: string(js),
|
||||
}
|
||||
}
|
||||
|
||||
// JSFuncCall calls a JavaScript function with the given arguments.
|
||||
//
|
||||
// It can be used in event handlers, e.g. onclick, onhover, etc. or
|
||||
// directly in HTML.
|
||||
func JSFuncCall[T ~string](functionName T, args ...any) ComponentScript {
|
||||
call := SafeScript(string(functionName), args...)
|
||||
sum := sha256.Sum256([]byte(call))
|
||||
return ComponentScript{
|
||||
Name: "jsFuncCall_" + hex.EncodeToString(sum[:]),
|
||||
// Function is empty because the body of the function is defined elsewhere,
|
||||
// e.g. in a <script> tag within a templ.Once block.
|
||||
Function: "",
|
||||
Call: call,
|
||||
CallInline: SafeScriptInline(string(functionName), args...),
|
||||
}
|
||||
}
|
189
js_test.go
Normal file
189
js_test.go
Normal file
@ -0,0 +1,189 @@
|
||||
package templ
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// Note: use of the RawEventHandler and JSFuncCall in ExpressionAttributes is tested in the parser package.
|
||||
|
||||
func TestJSUnsafeFuncCall(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
js string
|
||||
expected ComponentScript
|
||||
}{
|
||||
{
|
||||
name: "alert",
|
||||
js: "alert('hello')",
|
||||
expected: ComponentScript{
|
||||
Name: "jsUnsafeFuncCall_bc8b29d9abedc43cb4d79ec0af23be8c4255a4b76691aecf23ba3b0b8ab90011",
|
||||
Function: "",
|
||||
// Note that the Call field is attribute encoded.
|
||||
Call: "alert('hello')",
|
||||
// Whereas the CallInline field is what you would see inside a <script> tag.
|
||||
CallInline: "alert('hello')",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "immediately executed function",
|
||||
js: "(function(x) { x < 3 ? alert('hello less than 3') : alert('more than 3'); })(2);",
|
||||
expected: ComponentScript{
|
||||
Name: "jsUnsafeFuncCall_e4c24908f83227fd10c1a984fe8f99e15bfb3f195985af517253f6a72ec9106b",
|
||||
Function: "",
|
||||
Call: "(function(x) { x < 3 ? alert('hello less than 3') : alert('more than 3'); })(2);",
|
||||
CallInline: "(function(x) { x < 3 ? alert('hello less than 3') : alert('more than 3'); })(2);",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := JSUnsafeFuncCall(tt.js)
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSFuncCall(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
functionName string
|
||||
args []any
|
||||
expected ComponentScript
|
||||
expectedComponentOutput string
|
||||
}{
|
||||
{
|
||||
name: "no arguments are supported",
|
||||
functionName: "doSomething",
|
||||
args: nil,
|
||||
expected: ComponentScript{
|
||||
Name: "jsFuncCall_6e742483001f8d3c67652945c597b5a2025a7411cb9d1bae2f9e160bebfeb4c6",
|
||||
Function: "",
|
||||
Call: "doSomething()",
|
||||
CallInline: "doSomething()",
|
||||
},
|
||||
expectedComponentOutput: `<script type="text/javascript">doSomething()</script>`,
|
||||
},
|
||||
{
|
||||
name: "single argument is supported",
|
||||
functionName: "alert",
|
||||
args: []any{"hello"},
|
||||
expected: ComponentScript{
|
||||
Name: "jsFuncCall_92df7244f17dc5bfc41dfd02043df695e4664f8bf42c265a46d79b32b97693d0",
|
||||
Function: "",
|
||||
Call: "alert("hello")",
|
||||
CallInline: `alert("hello")`,
|
||||
},
|
||||
expectedComponentOutput: `<script type="text/javascript">alert("hello")</script>`,
|
||||
},
|
||||
{
|
||||
name: "multiple arguments are supported",
|
||||
functionName: "console.log",
|
||||
args: []any{"hello", "world"},
|
||||
expected: ComponentScript{
|
||||
Name: "jsFuncCall_2b3416c14fc2700d01e0013e7b7076bb8dd5f3126d19e2e801de409163e3960c",
|
||||
Function: "",
|
||||
Call: "console.log("hello","world")",
|
||||
CallInline: `console.log("hello","world")`,
|
||||
},
|
||||
expectedComponentOutput: `<script type="text/javascript">console.log("hello","world")</script>`,
|
||||
},
|
||||
{
|
||||
name: "attribute injection fails",
|
||||
functionName: `" onmouseover="alert('hello')`,
|
||||
args: nil,
|
||||
expected: ComponentScript{
|
||||
Name: "jsFuncCall_e56d1214f3b4fbf27406f209e3f4a58c2842fa2760b6d83da5ee72e04c89f913",
|
||||
Function: "",
|
||||
Call: "__templ_invalid_js_function_name()",
|
||||
CallInline: "__templ_invalid_js_function_name()",
|
||||
},
|
||||
expectedComponentOutput: `<script type="text/javascript">__templ_invalid_js_function_name()</script>`,
|
||||
},
|
||||
{
|
||||
name: "closing the script and injecting HTML fails",
|
||||
functionName: `</script><div>Hello</div><script>`,
|
||||
args: nil,
|
||||
expected: ComponentScript{
|
||||
Name: "jsFuncCall_e56d1214f3b4fbf27406f209e3f4a58c2842fa2760b6d83da5ee72e04c89f913",
|
||||
Function: "",
|
||||
Call: "__templ_invalid_js_function_name()",
|
||||
CallInline: "__templ_invalid_js_function_name()",
|
||||
},
|
||||
expectedComponentOutput: `<script type="text/javascript">__templ_invalid_js_function_name()</script>`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Test creation.
|
||||
actual := JSFuncCall(tt.functionName, tt.args...)
|
||||
if diff := cmp.Diff(tt.expected, actual); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
|
||||
// Test rendering.
|
||||
buf := new(bytes.Buffer)
|
||||
err := actual.Render(context.Background(), buf)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if diff := cmp.Diff(tt.expectedComponentOutput, buf.String()); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSFunctionNameRegexp(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
input: "console.log",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "alert",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "console.log('hello')",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "</script><div>Hello</div><script>",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: `" onmouseover="alert('hello')`,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "(new Date()).getTime",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "expressionThatReturnsAFunction()",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := jsFunctionName.MatchString(tt.input)
|
||||
if actual != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -103,41 +105,47 @@ type JSExpression string
|
||||
|
||||
// SafeScript encodes unknown parameters for safety for inside HTML attributes.
|
||||
func SafeScript(functionName string, params ...any) string {
|
||||
encodedParams := safeEncodeScriptParams(true, params)
|
||||
if !jsFunctionName.MatchString(functionName) {
|
||||
functionName = "__templ_invalid_js_function_name"
|
||||
}
|
||||
sb := new(strings.Builder)
|
||||
sb.WriteString(functionName)
|
||||
sb.WriteString(html.EscapeString(functionName))
|
||||
sb.WriteRune('(')
|
||||
sb.WriteString(strings.Join(encodedParams, ","))
|
||||
for i, p := range params {
|
||||
sb.WriteString(EscapeString(jsonEncodeParam(p)))
|
||||
if i < len(params)-1 {
|
||||
sb.WriteRune(',')
|
||||
}
|
||||
}
|
||||
sb.WriteRune(')')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// SafeScript encodes unknown parameters for safety for inline scripts.
|
||||
func SafeScriptInline(functionName string, params ...any) string {
|
||||
encodedParams := safeEncodeScriptParams(false, params)
|
||||
if !jsFunctionName.MatchString(functionName) {
|
||||
functionName = "__templ_invalid_js_function_name"
|
||||
}
|
||||
sb := new(strings.Builder)
|
||||
sb.WriteString(functionName)
|
||||
sb.WriteRune('(')
|
||||
sb.WriteString(strings.Join(encodedParams, ","))
|
||||
for i, p := range params {
|
||||
sb.WriteString(jsonEncodeParam(p))
|
||||
if i < len(params)-1 {
|
||||
sb.WriteRune(',')
|
||||
}
|
||||
}
|
||||
sb.WriteRune(')')
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func safeEncodeScriptParams(escapeHTML bool, params []any) []string {
|
||||
encodedParams := make([]string, len(params))
|
||||
for i := 0; i < len(encodedParams); i++ {
|
||||
if val, ok := params[i].(JSExpression); ok {
|
||||
encodedParams[i] = string(val)
|
||||
continue
|
||||
}
|
||||
|
||||
enc, _ := json.Marshal(params[i])
|
||||
if !escapeHTML {
|
||||
encodedParams[i] = string(enc)
|
||||
continue
|
||||
}
|
||||
encodedParams[i] = EscapeString(string(enc))
|
||||
func jsonEncodeParam(param any) string {
|
||||
if val, ok := param.(JSExpression); ok {
|
||||
return string(val)
|
||||
}
|
||||
|
||||
return encodedParams
|
||||
enc, _ := json.Marshal(param)
|
||||
return string(enc)
|
||||
}
|
||||
|
||||
// isValidJSFunctionName returns true if the given string is a valid JavaScript function name, e.g. console.log, alert, etc.
|
||||
var jsFunctionName = regexp.MustCompile(`^([$_a-zA-Z][$_a-zA-Z0-9]+\.?)+$`)
|
||||
|
Loading…
x
Reference in New Issue
Block a user