Skip to content

Commit

Permalink
feat: add templ.Once function (#750)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Davidson <[email protected]>
  • Loading branch information
a-h and joerdav authored May 23, 2024
1 parent b7a4eba commit e5633bb
Show file tree
Hide file tree
Showing 8 changed files with 428 additions and 3 deletions.
102 changes: 102 additions & 0 deletions docs/docs/03-syntax-and-usage/18-render-once.md
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>
}
```
7 changes: 7 additions & 0 deletions generator/test-once/expected.html
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'))">
23 changes: 23 additions & 0 deletions generator/test-once/render_test.go
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)
}
}
19 changes: 19 additions & 0 deletions generator/test-once/template.templ
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")
}
109 changes: 109 additions & 0 deletions generator/test-once/template_templ.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions once.go
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)
})
}
Loading

0 comments on commit e5633bb

Please sign in to comment.