Skip to content

Commit

Permalink
feat: add JSONString and JSONScript functions, update docs, refer to …
Browse files Browse the repository at this point in the history
…templ script as legacy in docs (#745)

Co-authored-by: Joe Davidson <joe.davidson.21111@gmail.com>
  • Loading branch information
a-h and joerdav committed May 21, 2024
1 parent 8665d5e commit 8e3d3ed
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 79 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ go tool covdata textfmt -i=./coverage/fmt,./coverage/generate,./coverage/version
go tool cover -func coverage.out | grep total
```

### test-cover-watch

```sh
gotestsum --watch -- -coverprofile=coverage.out
```

### benchmark

Run benchmarks.
Expand Down
142 changes: 139 additions & 3 deletions docs/docs/03-syntax-and-usage/12-script-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Scripts

Use standard `<script>` tags, and standard HTML attributes.
Use standard `<script>` tags, and standard HTML attributes to run JavaScript on the client.

```templ
templ body() {
Expand All @@ -17,7 +17,7 @@ templ body() {

## Importing scripts

You can also use standard `<script>` tags to load JavaScript from a URL.
Use standard `<script>` tags to load JavaScript from a URL.

```templ
templ head() {
Expand All @@ -27,7 +27,7 @@ templ head() {
}
```

You can then use the imported JavaScript directly in templ.
And use the imported JavaScript directly in templ via `<script>` tags.

```templ
templ body() {
Expand All @@ -50,8 +50,144 @@ templ body() {
}
```

:::tip
You can use a CDN to serve 3rd party scripts, or serve your own and 3rd party scripts from your server using a `http.FileServer`.

```go
mux := http.NewServeMux()
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets"))))
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(attributeData) }>Show alert</button>
}
```

```html title="output.html"
<button id="alerter" alert-data="{&quot;msg&quot;:&quot;Hello, from the attribute data&quot;}">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.

After transpilation and bundling, the output JavaScript code can be used in a web page by including a `<script>` tag.

### Creating a TypeScript project

Create a new TypeScript project with `npm`, and install TypeScript and `esbuild` as development dependencies.

```bash
mkdir ts
cd ts
npm init
npm install --save-dev typescript esbuild
```

Create a `src` directory to hold the TypeScript code.

```bash
mkdir src
```

And add a TypeScript file to the `src` directory.

```typescript title="ts/src/index.ts"
function hello() {
console.log('Hello, from TypeScript');
}
```

### Bundling TypeScript code

Add a script to build the TypeScript code in `index.ts` and copy it to an output directory (in this case `./assets/js/index.js`).

```json title="ts/package.json"
{
"name": "ts",
"version": "1.0.0",
"scripts": {
"build": "esbuild --bundle --minify --outfile=../assets/js/index.js ./src/index.ts"
},
"devDependencies": {
"esbuild": "0.21.3",
"typescript": "^5.4.5"
}
}
```

After running `npm build` in the `ts` directory, the TypeScript code is transpiled into JavaScript and copied to the output directory.

### Using the output JavaScript

The output file `../assets/js/index.js` can then be used in a templ project.

```templ title="components/head.templ"
templ head() {
<head>
<script src="/assets/js/index.js"></script>
</head>
}
```

You will need to configure your Go web server to serve the static content.

```go title="main.go"
func main() {
mux := http.NewServeMux()
// Serve the JS bundle.
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets"))))

// Serve components.
data := map[string]any{"msg": "Hello, World!"}
h := templ.Handler(components.Page(data))
mux.Handle("/", h)

fmt.Println("Listening on http://localhost:8080")
http.ListenAndServe("localhost:8080", mux)
}
```

## 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`.
:::

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

Here, the `page` HTML template includes a `script` element that loads a charting library, which is then used by the `body` element to render some data.
Expand Down
1 change: 1 addition & 0 deletions examples/content-security-policy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func (m *CSPMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
m.Log.Error("failed to generate nonce", slog.Any("error", err))
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
ctx := templ.WithNonce(r.Context(), nonce)
w.Header().Add("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))
Expand Down
38 changes: 2 additions & 36 deletions examples/typescript/components/index.templ
Original file line number Diff line number Diff line change
@@ -1,43 +1,9 @@
package components

import (
"encoding/json"
"fmt"
)

type Data struct {
Message string `json:"msg"`
}

func JSON(v any) (string, error) {
s, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(s), nil
}

func JSONScript(id string, data any) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
dataJSON, err := json.Marshal(data)
if err != nil {
return err
}
if _, err = io.WriteString(w, `<script`); err != nil {
return err
}
if id != "" {
if _, err = fmt.Fprintf(w, ` id="%s"`, templ.EscapeString(id)); err != nil {
return err
}
}
if _, err = fmt.Fprintf(w, ` type="application/json">%s</script>`, string(dataJSON)); err != nil {
return err
}
return nil
})
}

templ Page(attributeData Data, scriptData Data) {
<!DOCTYPE html>
<html>
Expand All @@ -46,8 +12,8 @@ templ Page(attributeData Data, scriptData Data) {
<script src="/assets/js/index.js" defer></script>
</head>
<body>
<button id="attributeAlerter" alert-data={ JSON(attributeData) }>Show alert from data in alert-data attribute</button>
@JSONScript("scriptData", scriptData)
<button id="attributeAlerter" alert-data={ templ.JSONString(attributeData) }>Show alert from data in alert-data attribute</button>
@templ.JSONScript("scriptData", scriptData)
<button id="scriptAlerter">Show alert from data in script</button>
</body>
</html>
Expand Down
40 changes: 3 additions & 37 deletions examples/typescript/components/index_templ.go

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

7 changes: 4 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,15 @@
pkgs.mkShell {
buildInputs = with pkgs; [
(golangci-lint.override { buildGoModule = buildGo121Module; })
cosign # Used to sign container images.
esbuild # Used to package JS examples.
go_1_21
gomod2nix.legacyPackages.${system}.gomod2nix
gopls
goreleaser
nodejs # Used to build templ-docs.
gotestsum
ko # Used to build Docker images.
cosign # Used to sign container images.
gomod2nix.legacyPackages.${system}.gomod2nix
nodejs # Used to build templ-docs.
xc.packages.${system}.xc
];
});
Expand Down
60 changes: 60 additions & 0 deletions jsonscript.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package templ

import (
"context"
"encoding/json"
"fmt"
"io"
)

var _ Component = JSONScriptElement{}

// JSONScript renders a JSON object inside a script element.
// e.g. <script type="application/json">{"foo":"bar"}</script>
func JSONScript(id string, data any) JSONScriptElement {
return JSONScriptElement{
ID: id,
Data: data,
Nonce: GetNonce,
}
}

func (j JSONScriptElement) WithNonceFromString(nonce string) JSONScriptElement {
j.Nonce = func(context.Context) string {
return nonce
}
return j
}

func (j JSONScriptElement) WithNonceFrom(f func(context.Context) string) JSONScriptElement {
j.Nonce = f
return j
}

type JSONScriptElement struct {
// ID of the element in the DOM.
ID string
// Data that will be encoded as JSON.
Data any
// Nonce is a function that returns a CSP nonce.
// Defaults to CSPNonceFromContext.
// See https://content-security-policy.com/nonce for more information.
Nonce func(ctx context.Context) string
}

func (j JSONScriptElement) Render(ctx context.Context, w io.Writer) (err error) {
var nonceAttr string
if nonce := j.Nonce(ctx); nonce != "" {
nonceAttr = fmt.Sprintf(" nonce=\"%s\"", EscapeString(nonce))
}
if _, err = fmt.Fprintf(w, "<script id=\"%s\" type=\"application/json\"%s>", EscapeString(j.ID), nonceAttr); err != nil {
return err
}
if err = json.NewEncoder(w).Encode(j.Data); err != nil {
return err
}
if _, err = io.WriteString(w, "</script>"); err != nil {
return err
}
return nil
}
Loading

0 comments on commit 8e3d3ed

Please sign in to comment.