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

47 changes: 47 additions & 0 deletions docs/docs/03-syntax-and-usage/11-script-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,50 @@ func main() {
}
}
```

You can also directly render the Javascript function, passing in Go data, using the `!` expression:
JustDerb marked this conversation as resolved.
Show resolved Hide resolved

```templ
package main

script printToConsole(content string) {
console.log(conent)
JustDerb marked this conversation as resolved.
Show resolved Hide resolved
}

templ page(content string) {
<html>
<body>
{! printToConsole(content) }
JustDerb marked this conversation as resolved.
Show resolved Hide resolved
</body>
</html>
}
```

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

```go
JustDerb marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
```
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 testscriptusage

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 testscriptusage

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.

52 changes: 48 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 ...interface{}) []string {
JustDerb marked this conversation as resolved.
Show resolved Hide resolved
JustDerb marked this conversation as resolved.
Show resolved Hide resolved
encodedParams := make([]string, len(params))
for i := 0; i < len(encodedParams); i++ {
enc, _ := json.Marshal(params[i])
encodedParams[i] = EscapeString(string(enc))
if escapeHtml {
encodedParams[i] = EscapeString(string(enc))
} else {
encodedParams[i] = string(enc)
}
JustDerb marked this conversation as resolved.
Show resolved Hide resolved
}
return encodedParams
}

// SafeScript encodes unknown parameters for safety for inside HTML attributes.
func SafeScript(functionName string, params ...interface{}) 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 ...interface{}) string {
encodedParams := safeEncodeScriptParams(false, params...)
sb := new(strings.Builder)
sb.WriteString(functionName)
sb.WriteRune('(')
Expand Down Expand Up @@ -550,9 +570,33 @@ type ComponentScript struct {
Name string
// Function to render.
Function string
// Call of the function in JavaScript syntax, including parameters.
// Call of the function in JavaScript syntax, including parameters, and ensures parameters are HTML escaped.
// e.g. print({ x: 1 })
Call string
// Call of the function in JavaScript syntax, including parameters. Doesn't not HTML escape parameters; useful for directly calling.
JustDerb marked this conversation as resolved.
Show resolved Hide resolved
// e.g. print({ x: 1 })
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