-
-
Notifications
You must be signed in to change notification settings - Fork 288
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add templ.Once function (#750)
Co-authored-by: Joe Davidson <[email protected]>
- Loading branch information
Showing
8 changed files
with
428 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
# Render once | ||
|
||
If you need to render something to the page once per page, you can create a `*OnceHandler` with `templ.NewOnceHandler()` and use its `Once()` method. | ||
|
||
The `*OnceHandler.Once()` method ensures that the content is only rendered once per distinct context passed to the component's `Render` method, even if the component is rendered multiple times. | ||
|
||
## Example | ||
|
||
The `hello` JavaScript function is only rendered once, even though the `hello` component is rendered twice. | ||
|
||
:::warning | ||
Dont write `@templ.NewOnceHandle().Once()` - this creates a new `*OnceHandler` each time the `Once` method is called, and will result in the content being rendered multiple times. | ||
::: | ||
|
||
```templ title="component.templ" | ||
package once | ||
var helloHandle = templ.NewOnceHandle() | ||
templ hello(label, name string) { | ||
@helloHandle.Once() { | ||
<script type="text/javascript"> | ||
function hello(name) { | ||
alert('Hello, ' + name + '!'); | ||
} | ||
</script> | ||
} | ||
<input type="button" value={ label } data-name={ name } onclick="hello(this.getAttribute('data-name'))"/> | ||
} | ||
templ page() { | ||
@hello("Hello User", "user") | ||
@hello("Hello World", "world") | ||
} | ||
``` | ||
|
||
```html title="Output" | ||
<script type="text/javascript"> | ||
function hello(name) { | ||
alert('Hello, ' + name + '!'); | ||
} | ||
</script> | ||
<input type="button" value="Hello User" data-name="user" onclick="hello(this.getAttribute('data-name'))"> | ||
<input type="button" value="Hello World" data-name="world" onclick="hello(this.getAttribute('data-name'))"> | ||
``` | ||
|
||
:::tip | ||
Note the use of the `data-name` attribute to pass the `name` value from server-side Go code to the client-side JavaScript code. | ||
|
||
The value of `name` is collected by the `onclick` handler, and passed to the `hello` function. | ||
|
||
To pass complex data structures, consider using a `data-` attribute to pass a JSON string using the `templ.JSONString` function, or use the `templ.JSONScript` function to create a templ component that creates a `<script>` element containing JSON data. | ||
::: | ||
|
||
## Common use cases | ||
|
||
- Rendering a `<style>` tag that contains CSS classes required by a component. | ||
- Rendering a `<script>` tag that contains JavaScript required by a component. | ||
- Rendering a `<link>` tag that contains a reference to a stylesheet. | ||
|
||
## Usage across packages | ||
|
||
Export a component that contains the `*OnceHandler` and the content to be rendered once. | ||
|
||
For example, create a `deps` package that contains a `JQuery` component that renders a `<script>` tag that references the jQuery library. | ||
|
||
```templ title="deps/deps.templ" | ||
package deps | ||
var jqueryHandle = templ.NewOnceHandle() | ||
templ JQuery() { | ||
@jqueryHandle.Once() { | ||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> | ||
} | ||
} | ||
``` | ||
|
||
You can then use the `JQuery` component in other packages, and the jQuery library will only be included once in the rendered HTML. | ||
|
||
```templ title="main.templ" | ||
package main | ||
import "deps" | ||
templ page() { | ||
<html> | ||
<head> | ||
@deps.JQuery() | ||
</head> | ||
<body> | ||
<h1>Hello, World!</h1> | ||
@button() | ||
</body> | ||
</html> | ||
} | ||
templ button() { | ||
@deps.JQuery() | ||
<button>Click me</button> | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<script type="text/javascript"> | ||
function hello(name) { | ||
alert('Hello, ' + name + '!'); | ||
} | ||
</script> | ||
<input type="button" value="Hello User" data-name="user" onclick="hello(this.getAttribute('data-name'))"> | ||
<input type="button" value="Hello World" data-name="world" onclick="hello(this.getAttribute('data-name'))"> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package once | ||
|
||
import ( | ||
_ "embed" | ||
"testing" | ||
|
||
"github.com/a-h/templ/generator/htmldiff" | ||
) | ||
|
||
//go:embed expected.html | ||
var expected string | ||
|
||
func Test(t *testing.T) { | ||
component := render() | ||
|
||
diff, err := htmldiff.Diff(component, expected) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if diff != "" { | ||
t.Error(diff) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package once | ||
|
||
var helloHandle = templ.NewOnceHandle() | ||
|
||
templ hello(label, name string) { | ||
@helloHandle.Once() { | ||
<script type="text/javascript"> | ||
function hello(name) { | ||
alert('Hello, ' + name + '!'); | ||
} | ||
</script> | ||
} | ||
<input type="button" value={ label } data-name={ name } onclick="hello(this.getAttribute('data-name'))"/> | ||
} | ||
|
||
templ render() { | ||
@hello("Hello User", "user") | ||
@hello("Hello World", "world") | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package templ | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"sync/atomic" | ||
) | ||
|
||
// onceHandleIndex is used to identify unique once handles in a program run. | ||
var onceHandleIndex int64 | ||
|
||
// NewOnceHandle creates a OnceHandle used to ensure that the children of its | ||
// `Once` method are only rendered once per context. | ||
func NewOnceHandle() *OnceHandle { | ||
return &OnceHandle{ | ||
id: atomic.AddInt64(&onceHandleIndex, 1), | ||
} | ||
} | ||
|
||
// OnceHandle is used to ensure that the children of its `Once` method are are only | ||
// rendered once per context. | ||
type OnceHandle struct { | ||
// id is used to identify which instance of the OnceHandle is being used. | ||
// The OnceHandle can't be an empty struct, because: | ||
// | ||
// | Two distinct zero-size variables may | ||
// | have the same address in memory | ||
// | ||
// https://go.dev/ref/spec#Size_and_alignment_guarantees | ||
id int64 | ||
} | ||
|
||
// Once returns a component that renders its children once per context. | ||
func (o *OnceHandle) Once() Component { | ||
return ComponentFunc(func(ctx context.Context, w io.Writer) (err error) { | ||
_, v := getContext(ctx) | ||
if v.getHasBeenRendered(o) { | ||
return nil | ||
} | ||
v.setHasBeenRendered(o) | ||
return GetChildren(ctx).Render(ctx, w) | ||
}) | ||
} |
Oops, something went wrong.