diff --git a/cmd/templ/visualize/sourcemapvisualisation_templ.go b/cmd/templ/visualize/sourcemapvisualisation_templ.go index 4aa38e8f8..d63b053cf 100644 --- a/cmd/templ/visualize/sourcemapvisualisation_templ.go +++ b/cmd/templ/visualize/sourcemapvisualisation_templ.go @@ -179,7 +179,8 @@ func highlight(sourceId, targetId string) templ.ComponentScript { for(let i = 0; i < items.length; i ++) { items[i].classList.add("highlighted"); }}`, - Call: templ.SafeScript(`__templ_highlight_ae80`, sourceId, targetId), + Call: templ.SafeScript(`__templ_highlight_ae80`, sourceId, targetId), + CallInline: templ.SafeScriptInline(`__templ_highlight_ae80`, sourceId, targetId), } } @@ -194,7 +195,8 @@ func removeHighlight(sourceId, targetId string) templ.ComponentScript { for(let i = 0; i < items.length; i ++) { items[i].classList.remove("highlighted"); }}`, - Call: templ.SafeScript(`__templ_removeHighlight_58f2`, sourceId, targetId), + Call: templ.SafeScript(`__templ_removeHighlight_58f2`, sourceId, targetId), + CallInline: templ.SafeScriptInline(`__templ_removeHighlight_58f2`, sourceId, targetId), } } diff --git a/docs/docs/03-syntax-and-usage/11-script-templates.md b/docs/docs/03-syntax-and-usage/11-script-templates.md index 892564d3b..25694c7ca 100644 --- a/docs/docs/03-syntax-and-usage/11-script-templates.md +++ b/docs/docs/03-syntax-and-usage/11-script-templates.md @@ -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 ( @@ -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) { + + + @printToConsole(content) + @printToConsole(fmt.Sprintf("Again: %s", content)) + + +} +``` + +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" + + + + + + + +``` diff --git a/examples/external-libraries/components_templ.go b/examples/external-libraries/components_templ.go index a23e13d4c..fc80ecf07 100644 --- a/examples/external-libraries/components_templ.go +++ b/examples/external-libraries/components_templ.go @@ -15,7 +15,8 @@ func graph(data []TimeValue) templ.ComponentScript { Function: `function __templ_graph_c2ba(data){const chart = LightweightCharts.createChart(document.body, { width: 400, height: 300 }); const lineSeries = chart.addLineSeries(); lineSeries.setData(data);}`, - Call: templ.SafeScript(`__templ_graph_c2ba`, data), + Call: templ.SafeScript(`__templ_graph_c2ba`, data), + CallInline: templ.SafeScriptInline(`__templ_graph_c2ba`, data), } } diff --git a/generator/generator.go b/generator/generator.go index bbbeb2877..faf7acfbc 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -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-- } // } diff --git a/generator/test-script-inline/expected.html b/generator/test-script-inline/expected.html new file mode 100644 index 000000000..99c37df34 --- /dev/null +++ b/generator/test-script-inline/expected.html @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/generator/test-script-inline/render_test.go b/generator/test-script-inline/render_test.go new file mode 100644 index 000000000..0507e448c --- /dev/null +++ b/generator/test-script-inline/render_test.go @@ -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) + } +} diff --git a/generator/test-script-inline/template.templ b/generator/test-script-inline/template.templ new file mode 100644 index 000000000..f85b35726 --- /dev/null +++ b/generator/test-script-inline/template.templ @@ -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) +} diff --git a/generator/test-script-inline/template_templ.go b/generator/test-script-inline/template_templ.go new file mode 100644 index 000000000..54b9d93f5 --- /dev/null +++ b/generator/test-script-inline/template_templ.go @@ -0,0 +1,64 @@ +// Code generated by templ - DO NOT EDIT. + +package testscriptinline + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func withParameters(a string, b string, c int) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_withParameters_1056`, + Function: `function __templ_withParameters_1056(a, b, c){console.log(a, b, c);}`, + Call: templ.SafeScript(`__templ_withParameters_1056`, a, b, c), + CallInline: templ.SafeScriptInline(`__templ_withParameters_1056`, a, b, c), + } +} + +func withoutParameters() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_withoutParameters_6bbf`, + Function: `function __templ_withoutParameters_6bbf(){alert("hello");}`, + Call: templ.SafeScript(`__templ_withoutParameters_6bbf`), + CallInline: templ.SafeScriptInline(`__templ_withoutParameters_6bbf`), + } +} + +func InlineJavascript(a string) 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) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = withoutParameters().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = withParameters(a, "test", 123).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = withoutParameters().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = withParameters(a, "test", 123).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/generator/test-script-usage/template_templ.go b/generator/test-script-usage/template_templ.go index 37c43f831..c8be34d3a 100644 --- a/generator/test-script-usage/template_templ.go +++ b/generator/test-script-usage/template_templ.go @@ -11,25 +11,28 @@ import "bytes" func withParameters(a string, b string, c int) templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_withParameters_1056`, - Function: `function __templ_withParameters_1056(a, b, c){console.log(a, b, c);}`, - Call: templ.SafeScript(`__templ_withParameters_1056`, a, b, c), + Name: `__templ_withParameters_1056`, + Function: `function __templ_withParameters_1056(a, b, c){console.log(a, b, c);}`, + Call: templ.SafeScript(`__templ_withParameters_1056`, a, b, c), + CallInline: templ.SafeScriptInline(`__templ_withParameters_1056`, a, b, c), } } func withoutParameters() templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_withoutParameters_6bbf`, - Function: `function __templ_withoutParameters_6bbf(){alert("hello");}`, - Call: templ.SafeScript(`__templ_withoutParameters_6bbf`), + Name: `__templ_withoutParameters_6bbf`, + Function: `function __templ_withoutParameters_6bbf(){alert("hello");}`, + Call: templ.SafeScript(`__templ_withoutParameters_6bbf`), + CallInline: templ.SafeScriptInline(`__templ_withoutParameters_6bbf`), } } func onClick() templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_onClick_657d`, - Function: `function __templ_onClick_657d(){alert("clicked");}`, - Call: templ.SafeScript(`__templ_onClick_657d`), + Name: `__templ_onClick_657d`, + Function: `function __templ_onClick_657d(){alert("clicked");}`, + Call: templ.SafeScript(`__templ_onClick_657d`), + CallInline: templ.SafeScriptInline(`__templ_onClick_657d`), } } @@ -170,9 +173,10 @@ func ThreeButtons() templ.Component { func conditionalScript() templ.ComponentScript { return templ.ComponentScript{ - Name: `__templ_conditionalScript_de41`, - Function: `function __templ_conditionalScript_de41(){alert("conditional");}`, - Call: templ.SafeScript(`__templ_conditionalScript_de41`), + Name: `__templ_conditionalScript_de41`, + Function: `function __templ_conditionalScript_de41(){alert("conditional");}`, + Call: templ.SafeScript(`__templ_conditionalScript_de41`), + CallInline: templ.SafeScriptInline(`__templ_conditionalScript_de41`), } } diff --git a/runtime.go b/runtime.go index 75a694cc1..c6f618d95 100644 --- a/runtime.go +++ b/runtime.go @@ -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('(') @@ -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("some string",12345)) + // + // This is can be injected into HTML attributes: + // 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: + // + 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, ``); err != nil { + return err + } + } + return nil } // RenderScriptItems renders a