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:
parent
190ddba8f2
commit
85a7b8b19a
@ -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.
|
||||
|
@ -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="{"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.
|
||||
|
||||
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.
|
||||
|
@ -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))
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
60
jsonscript.go
Normal 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
53
jsonscript_test.go
Normal 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
14
jsonstring.go
Normal 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
28
jsonstring_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user