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

How to inject a standalone <script> tag with data injected #231

Closed
JustDerb opened this issue Oct 14, 2023 · 3 comments · Fixed by #285
Closed

How to inject a standalone <script> tag with data injected #231

JustDerb opened this issue Oct 14, 2023 · 3 comments · Fixed by #285

Comments

@JustDerb
Copy link
Contributor

Relevant guide I was following: https://templ.guide/syntax-and-usage/script-templates#script-templates

I'm trying to add JavaScript at the end of the HTML (not inject it into an HTML attribute)

package main

script example(message string) {
    console.log(message);
}

templ page(message string) {
    <html>
        <body>
            <script>
                { example(message) }
            </script>
        </body>
    </html>
}

I'm not sure if this is possible with Templ, but it'd be really nice if it was! Maybe I'm holding it wrong?? Any pointers would be appreciated.

Expected

If passed in Hello, world!:

    <html>
        <body>
            <script>
                console.log("Hello, world!");
            </script>
        </body>
    </html>

Actual

If passed in Hello, world!:

    <html>
        <body>
            <script>
                { example(message) }
            </script>
        </body>
    </html>
@a-h
Copy link
Owner

a-h commented Oct 14, 2023

Hi, thanks for raising this.

We've discussed it in #126, the current solution to that is defined here #126 (comment)

The current design of script templates is that they're functions with a fixed body. Any variables that are passed to script templates from Go code can then be properly sanitized to prevent XSS by JSON serializing them.

Because scripts have a fixed body, it also makes it possible to identify that they've already been rendered to the output, and not to do it multiple times (allowing you to register per-component scripts).

Would love your input in #126 on whether that meets your needs.

@JustDerb
Copy link
Contributor Author

Thanks for the comment link; I took that solution and tried to apply it, but it looks like it only works with zero-parameter functions? This is the updated code using that function, note that I needed to add script.Function so that the function was actually defined:

func onLoad(script templ.ComponentScript) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
		if _, err = io.WriteString(w, `<script type="text/javascript">`+"\r\n"+script.Function+"\r\n"+script.Call+"\r\n</script>"); err != nil {
			return err
		}
		return nil
	})
}

script example(message string) {
    console.log(message);
}

templ page(message string) {
    <html>
        <body>
            @onLoad(example(message))
        </body>
    </html>
}

Expected

<html>
    <body>
        <script type="text/javascript">
            __templ_example_9b63("Hello, World!")
        </script>
    </body>
</html>

Actual

<html>
    <body>
        <script type="text/javascript">
            __templ_example_9b63(&#34;Hello, World!&#34;)
        </script>
    </body>
</html>

I believe this is because the generated code tries to sanitize the input:

func example(message string) templ.ComponentScript {
	return templ.ComponentScript{
		Name:     `__templ_example_9b63`,
		Function: `function __templ_example_9b63(message){console.log(message);}`,
		Call:     templ.SafeScript(`__templ_example_9b63`, message),
	}
}

I'm not sure how to tell templ to not apply this sanitation... It seems to me that SafeScript is encoding the parameters wrong, or maybe too aggressively? Or it's not encapsulating the types correctly? For example, in SafeScript, a String passed into the params ...interface{} should serialize the string, encoding \ and ", and then surrounding in ".

@JustDerb
Copy link
Contributor Author

JustDerb commented Oct 16, 2023

This code below works, but is not great in terms of ergonomics. You have to specify your parameters twice to get the generation of the script template to work, and then pass them in again to feed into onLoad which now is coded to bypass the templ.SafeScript call:

import "encoding/json"

func onLoad(script templ.ComponentScript, params ...interface{}) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
		if _, err = io.WriteString(w, `<script type="text/javascript">`+"\r\n"+script.Function+"\r\n"+script.Name+"("); err != nil {
			return err
		}
		paramsLen := len(params)
		for i, param := range params {
			paramEncodedBytes, err := json.Marshal(param)
			if err != nil {
				return err
			}
			if _, err = w.Write(paramEncodedBytes); err != nil {
				return err
			}
			if i + 1 != paramsLen {
				if _, err = io.WriteString(w, ", "); err != nil {
					return err
				}
			}	
		}
		if _, err = io.WriteString(w, ")\r\n</script>"); err != nil {
			return err
		}
		return nil
	})
}

script example(message string) {
    console.log(message);
}

templ TestPage(message string) {
    <html>
        <body>
            @onLoad(example(message), message)
        </body>
    </html>
}

This generates:

<html>
    <body>
        <script type="text/javascript">
            function __templ_example_9b63(message){console.log(message);}
            __templ_example_9b63("Hello, World!")
        </script>
    </body>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants