1
0
mirror of https://github.com/a-h/templ.git synced 2025-02-06 09:45:21 +00:00

feat: add JSONString and JSONScript functions, update docs, refer to templ script as legacy in docs (#745)

Co-authored-by: Joe Davidson <joe.davidson.21111@gmail.com>
This commit is contained in:
Adrian Hesketh 2024-05-21 14:27:08 +01:00 committed by GitHub
parent 190ddba8f2
commit 85a7b8b19a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 314 additions and 80 deletions

View File

@ -1 +1 @@
0.2.698
0.2.700

View File

@ -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.

View File

@ -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() {
@ -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() {
@ -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() {
@ -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.

View File

@ -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))

View File

@ -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>
@ -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>

View File

@ -9,44 +9,10 @@ import "context"
import "io"
import "bytes"
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
})
}
func Page(attributeData Data, scriptData Data) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
@ -65,9 +31,9 @@ func Page(attributeData Data, scriptData Data) templ.Component {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(JSON(attributeData))
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSONString(attributeData))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/typescript/components/index.templ`, Line: 49, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/typescript/components/index.templ`, Line: 15, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
@ -77,7 +43,7 @@ func Page(attributeData Data, scriptData Data) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = JSONScript("scriptData", scriptData).Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = templ.JSONScript("scriptData", scriptData).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@ -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
];
});

60
jsonscript.go Normal file
View File

@ -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
}

53
jsonscript_test.go Normal file
View File

@ -0,0 +1,53 @@
package templ_test
import (
"bytes"
"context"
"testing"
"github.com/a-h/templ"
"github.com/google/go-cmp/cmp"
)
func TestJSONScriptElement(t *testing.T) {
data := map[string]interface{}{"foo": "bar"}
tests := []struct {
name string
ctx context.Context
e templ.JSONScriptElement
expected string
}{
{
name: "renders data as JSON inside a script element",
e: templ.JSONScript("id", data),
expected: "<script id=\"id\" type=\"application/json\">{\"foo\":\"bar\"}\n</script>",
},
{
name: "if a nonce is available in the context, it is used",
ctx: templ.WithNonce(context.Background(), "nonce-from-context"),
e: templ.JSONScript("idc", data),
expected: "<script id=\"idc\" type=\"application/json\" nonce=\"nonce-from-context\">{\"foo\":\"bar\"}\n</script>",
},
{
name: "if a nonce is provided, it is used",
e: templ.JSONScript("ids", data).WithNonceFromString("nonce-from-string"),
expected: "<script id=\"ids\" type=\"application/json\" nonce=\"nonce-from-string\">{\"foo\":\"bar\"}\n</script>",
},
{
name: "if a nonce function is provided, it is used",
e: templ.JSONScript("idf", data).WithNonceFrom(func(context.Context) string { return "nonce-from-function" }),
expected: "<script id=\"idf\" type=\"application/json\" nonce=\"nonce-from-function\">{\"foo\":\"bar\"}\n</script>",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := new(bytes.Buffer)
if err := tt.e.Render(tt.ctx, w); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if diff := cmp.Diff(tt.expected, w.String()); diff != "" {
t.Fatalf("unexpected output (-want +got):\n%s", diff)
}
})
}
}

14
jsonstring.go Normal file
View File

@ -0,0 +1,14 @@
package templ
import (
"encoding/json"
)
// JSONString returns a JSON encoded string of v.
func JSONString(v any) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
}

28
jsonstring_test.go Normal file
View File

@ -0,0 +1,28 @@
package templ_test
import (
"testing"
"github.com/a-h/templ"
)
func TestJSONString(t *testing.T) {
t.Run("renders input data as a JSON string", func(t *testing.T) {
data := map[string]any{"foo": "bar"}
actual, err := templ.JSONString(data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := "{\"foo\":\"bar\"}"
if actual != expected {
t.Fatalf("unexpected output: want %q, got %q", expected, actual)
}
})
t.Run("returns an error if the data cannot be marshalled", func(t *testing.T) {
data := make(chan int)
_, err := templ.JSONString(data)
if err == nil {
t.Fatalf("expected an error, got nil")
}
})
}

View File

@ -51,6 +51,9 @@ func WithNonce(ctx context.Context, nonce string) context.Context {
// GetNonce returns the CSP nonce value set with WithNonce, or an
// empty string if none has been set.
func GetNonce(ctx context.Context) (nonce string) {
if ctx == nil {
return ""
}
_, v := getContext(ctx)
return v.nonce
}