Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow script components to be rendered inline #285

Merged
merged 4 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cmd/templ/visualize/sourcemapvisualisation_templ.go

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

64 changes: 63 additions & 1 deletion docs/docs/03-syntax-and-usage/11-script-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ templ page(data []TimeValue) {

The data is loaded by the backend into the template. This example uses a constant, but it could easily have collected the `[]TimeValue` from a database.

```go
```go title="main.go"
package main

import (
Expand Down Expand Up @@ -103,3 +103,65 @@ func main() {
}
}
```

`script` elements are templ Components, so you can also directly render the Javascript function, passing in Go data, using the `@` expression:

```templ
package main

import "fmt"

script printToConsole(content string) {
console.log(content)
}

templ page(content string) {
<html>
<body>
@printToConsole(content)
@printToConsole(fmt.Sprintf("Again: %s", content))
</body>
</html>
}
```

The data passed into the Javascript funtion will be JSON encoded, which then can be used inside the function.

```go title="main.go"
package main

import (
"fmt"
"log"
"net/http"
"time"
)

func main() {
mux := http.NewServeMux()

// Handle template.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Format the current time and pass it into our template
page(time.Now().String()).Render(r.Context(), w)
})

// Start the server.
fmt.Println("listening on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Printf("error listening: %v", err)
}
}
```

After building and running the executable, running `curl http://localhost:8080/` would render:

```html title="Output"
<html>
<body>
<script type="text/javascript">function __templ_printToConsole_5a85(content){console.log(content)}</script>
<script type="text/javascript">__templ_printToConsole_5a85("2023-11-11 01:01:40.983381358 +0000 UTC")</script>
<script type="text/javascript">__templ_printToConsole_5a85("Again: 2023-11-11 01:01:40.983381358 +0000 UTC")</script>
</body>
</html>
```
3 changes: 2 additions & 1 deletion examples/external-libraries/components_templ.go

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

4 changes: 4 additions & 0 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,10 @@ func (g *generator) writeScript(t parser.ScriptTemplate) error {
if _, err = g.w.WriteIndent(indentLevel, "Call: templ.SafeScript("+goFn+", "+stripTypes(t.Parameters.Value)+"),\n"); err != nil {
return err
}
// CallInline: templ.SafeScriptInline(scriptName, a, b, c)
if _, err = g.w.WriteIndent(indentLevel, "CallInline: templ.SafeScriptInline("+goFn+", "+stripTypes(t.Parameters.Value)+"),\n"); err != nil {
return err
}
indentLevel--
}
// }
Expand Down
18 changes: 18 additions & 0 deletions generator/test-script-inline/expected.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script type="text/javascript">
function __templ_withoutParameters_6bbf(){alert("hello");}
</script>
<script type="text/javascript">
__templ_withoutParameters_6bbf()
</script>
<script type="text/javascript">
function __templ_withParameters_1056(a, b, c){console.log(a, b, c);}
</script>
<script type="text/javascript">
__templ_withParameters_1056("injected","test",123)
</script>
<script type="text/javascript">
__templ_withoutParameters_6bbf()
</script>
<script type="text/javascript">
__templ_withParameters_1056("injected","test",123)
</script>
23 changes: 23 additions & 0 deletions generator/test-script-inline/render_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package testscriptinline

import (
_ "embed"
"testing"

"github.com/a-h/templ/generator/htmldiff"
)

//go:embed expected.html
var expected string

func Test(t *testing.T) {
component := InlineJavascript("injected")

diff, err := htmldiff.Diff(component, expected)
if err != nil {
t.Fatal(err)
}
if diff != "" {
t.Error(diff)
}
}
17 changes: 17 additions & 0 deletions generator/test-script-inline/template.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package testscriptinline

script withParameters(a string, b string, c int) {
console.log(a, b, c);
}

script withoutParameters() {
alert("hello");
}

templ InlineJavascript(a string) {
@withoutParameters()
@withParameters(a, "test", 123)
// Call once more, to ensure it's defined only once
@withoutParameters()
@withParameters(a, "test", 123)
}
64 changes: 64 additions & 0 deletions generator/test-script-inline/template_templ.go

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

28 changes: 16 additions & 12 deletions generator/test-script-usage/template_templ.go

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

70 changes: 66 additions & 4 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,13 +471,33 @@ type SafeURL string

// Script handling.

// SafeScript encodes unknown parameters for safety.
func SafeScript(functionName string, params ...interface{}) string {
func safeEncodeScriptParams(escapeHTML bool, params ...any) []string {
encodedParams := make([]string, len(params))
for i := 0; i < len(encodedParams); i++ {
enc, _ := json.Marshal(params[i])
if !escapeHTML {
encodedParams[i] = string(enc)
continue
}
encodedParams[i] = EscapeString(string(enc))
}
return encodedParams
}

// SafeScript encodes unknown parameters for safety for inside HTML attributes.
func SafeScript(functionName string, params ...any) string {
encodedParams := safeEncodeScriptParams(true, params)
sb := new(strings.Builder)
sb.WriteString(functionName)
sb.WriteRune('(')
sb.WriteString(strings.Join(encodedParams, ","))
sb.WriteRune(')')
return sb.String()
}

// SafeScript encodes unknown parameters for safety for inline scripts.
func SafeScriptInline(functionName string, params ...any) string {
encodedParams := safeEncodeScriptParams(false, params)
sb := new(strings.Builder)
sb.WriteString(functionName)
sb.WriteRune('(')
Expand Down Expand Up @@ -550,9 +570,51 @@ type ComponentScript struct {
Name string
// Function to render.
Function string
// Call of the function in JavaScript syntax, including parameters.
// e.g. print({ x: 1 })
// Call of the function in JavaScript syntax, including parameters, and
// ensures parameters are HTML escaped; useful for injecting into HTML
// attributes like onclick, onhover, etc.
//
// Given:
// functionName("some string",12345)
// It would render:
// __templ_functionName_sha(&#34;some string&#34;,12345))
//
// This is can be injected into HTML attributes:
// <button onClick="__templ_functionName_sha(&#34;some string&#34;,12345))">Click Me</button>
Call string
// Call of the function in JavaScript syntax, including parameters. It
// does not HTML escape parameters; useful for directly calling in script
// elements.
//
// Given:
// functionName("some string",12345)
// It would render:
// __templ_functionName_sha("some string",12345))
//
// This is can be used to call the function inside a script tag:
// <script>__templ_functionName_sha("some string",12345))</script>
CallInline string
}

var _ Component = ComponentScript{}

func (c ComponentScript) Render(ctx context.Context, w io.Writer) error {
JustDerb marked this conversation as resolved.
Show resolved Hide resolved
err := RenderScriptItems(ctx, w, c)
if err != nil {
return err
}
if len(c.Call) > 0 {
if _, err = io.WriteString(w, `<script type="text/javascript">`); err != nil {
return err
}
if _, err = io.WriteString(w, c.CallInline); err != nil {
return err
}
if _, err = io.WriteString(w, `</script>`); err != nil {
return err
}
}
return nil
}

// RenderScriptItems renders a <script> element, if the script has not already been rendered.
Expand Down
Loading