Skip to content

Commit

Permalink
feat: add ability to call script components inline (#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
JustDerb authored Nov 13, 2023
1 parent b9e33d7 commit abe23c6
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 20 deletions.
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 @@ -456,13 +456,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 @@ -535,9 +555,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 {
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

0 comments on commit abe23c6

Please sign in to comment.