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

feat: ComponentScript spread attribute support #595

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.608
0.2.610
1 change: 1 addition & 0 deletions docs/docs/03-syntax-and-usage/03-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ It's possible to spread any variable of type `templ.Attributes`. `templ.Attribut
* If the value is a `bool`, the attribute is added as a boolean attribute if the value is true, e.g. `<div name>`.
* If the value is a `templ.KeyValue[string, bool]`, the attribute is added if the boolean is true, e.g. `<div name="value">`.
* If the value is a `templ.KeyValue[bool, bool]`, the attribute is added if both boolean values are true, as `<div name>`.
* If the value is a `templ.ComponentScript`, the relevant `<script>` element will be rendered into the HTML and the generated function call will be inserted into the value of the attribute.

```templ
templ component(shouldBeUsed bool, attrs templ.Attributes) {
Expand Down
33 changes: 25 additions & 8 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1021,15 +1021,25 @@ func isScriptAttribute(name string) bool {

func (g *generator) writeElementScript(indentLevel int, n parser.Element) (err error) {
var scriptExpressions []string
var spreadAttributes []string
for _, attr := range n.Attributes {
scriptExpressions = append(scriptExpressions, getAttributeScripts(attr)...)
scripts, spreads := getAttributeScripts(attr)
scriptExpressions = append(scriptExpressions, scripts...)
spreadAttributes = append(spreadAttributes, spreads...)
}
if len(scriptExpressions) == 0 {
if len(scriptExpressions) == 0 && len(spreadAttributes) == 0 {
return
}

renderFuncString := "RenderScriptItems"
spreadParamString := ""
if len(spreadAttributes) > 0 {
renderFuncString = "RenderScriptItemsWithSpread"
spreadParamString = "[]templ.Attributes{" + strings.Join(spreadAttributes, ", ") + "}, "
}
// Render the scripts before the element if required.
// templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, a, b, c)
if _, err = g.w.WriteIndent(indentLevel, "templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, "+strings.Join(scriptExpressions, ", ")+")\n"); err != nil {
// templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, []Attributes, a, b, c)
if _, err = g.w.WriteIndent(indentLevel, "templ_7745c5c3_Err = templ."+renderFuncString+"(ctx, templ_7745c5c3_Buffer, "+spreadParamString+strings.Join(scriptExpressions, ", ")+")\n"); err != nil {
return err
}
if err = g.writeErrorHandler(indentLevel); err != nil {
Expand All @@ -1038,13 +1048,17 @@ func (g *generator) writeElementScript(indentLevel int, n parser.Element) (err e
return err
}

func getAttributeScripts(attr parser.Attribute) (scripts []string) {
func getAttributeScripts(attr parser.Attribute) (scripts []string, spreads []string) {
if attr, ok := attr.(parser.ConditionalAttribute); ok {
for _, attr := range attr.Then {
scripts = append(scripts, getAttributeScripts(attr)...)
thenScripts, thenSpreads := getAttributeScripts(attr)
scripts = append(scripts, thenScripts...)
spreads = append(spreads, thenSpreads...)
}
for _, attr := range attr.Else {
scripts = append(scripts, getAttributeScripts(attr)...)
elseScripts, elseSpreads := getAttributeScripts(attr)
scripts = append(scripts, elseScripts...)
spreads = append(spreads, elseSpreads...)
}
}
if attr, ok := attr.(parser.ExpressionAttribute); ok {
Expand All @@ -1053,7 +1067,10 @@ func getAttributeScripts(attr parser.Attribute) (scripts []string) {
scripts = append(scripts, attr.Expression.Value)
}
}
return scripts
if attr, ok := attr.(parser.SpreadAttributes); ok {
spreads = append(spreads, attr.Expression.Value)
}
return
}

func (g *generator) writeBoolConstantAttribute(indentLevel int, attr parser.BoolConstantAttribute) (err error) {
Expand Down
10 changes: 7 additions & 3 deletions generator/test-spread-attributes/expected.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<div>
<a bool dateid="my-custom-id" hx-get="/page" id="test" nonshade optional-from-func-true text="lorem">
<script type="text/javascript">
function __templ_BasicScript_97f1(message){alert(message);
}
</script>
<a bool dateid="my-custom-id" hx-get="/page" id="test" nonshade onclick="__templ_BasicScript_97f1(&#34;hello, world&#34;)" optional-from-func-true text="lorem" href="/path/to/resource" onmouseleave="__templ_BasicScript_97f1(&#34;mouse left&#34;)">
text
</a>
<div bool dateid="my-custom-id" hx-get="/page" id="test" nonshade optional-from-func-true text="lorem">
<button onclick="__templ_BasicScript_97f1(&#34;you clicked on me&#34;)" bool dateid="my-custom-id" hx-get="/page" id="test" nonshade onclick="__templ_BasicScript_97f1(&#34;hello, world&#34;)" optional-from-func-true text="lorem">
text2
</div>
</button>
<div>
text3
</div>
Expand Down
59 changes: 33 additions & 26 deletions generator/test-spread-attributes/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,39 @@ import (
var expected string

func Test(t *testing.T) {
component := BasicTemplate(templ.Attributes{
// Should render as `bool` as the value is true, and the conditional render is also true.
"bool": templ.KV(true, true),
// Should not render, as the conditional render value is false.
"bool-disabled": templ.KV(true, false),
// Should render as `dateId="my-custom-id"`.
"dateId": "my-custom-id",
// Should render as `hx-get="/page"`.
"hx-get": "/page",
// Should render as `id="test"`.
"id": "test",
// Should not render, as the attribute value, and the conditional render value is false.
"no-bool": templ.KV(false, false),
// Should not render, as the conditional render value is false.
"no-text": templ.KV("empty", false),
// Should render as `nonshare`, as the value is true.
"nonshade": true,
// Should not render, as the value is false.
"shade": false,
// Should render text="lorem" as the value is true.
"text": templ.KV("lorem", true),
// Optional attribute based on result of func() bool.
"optional-from-func-false": func() bool { return false },
// Optional attribute based on result of func() bool.
"optional-from-func-true": func() bool { return true },
})
component := BasicTemplate(
templ.Attributes{
// Should render as `bool` as the value is true, and the conditional render is also true.
"bool": templ.KV(true, true),
// Should not render, as the conditional render value is false.
"bool-disabled": templ.KV(true, false),
// Should render as `dateId="my-custom-id"`.
"dateId": "my-custom-id",
// Should render as `hx-get="/page"`.
"hx-get": "/page",
// Should render as `id="test"`.
"id": "test",
// Should not render, as the attribute value, and the conditional render value is false.
"no-bool": templ.KV(false, false),
// Should not render, as the conditional render value is false.
"no-text": templ.KV("empty", false),
// Should render as `nonshare`, as the value is true.
"nonshade": true,
// Should not render, as the value is false.
"shade": false,
// Should render text="lorem" as the value is true.
"text": templ.KV("lorem", true),
// Optional attribute based on result of func() bool.
"optional-from-func-false": func() bool { return false },
// Optional attribute based on result of func() bool.
"optional-from-func-true": func() bool { return true },
// Should render a <script> element before the element and include a call to the JS function as an attribute
"onclick": BasicScript("hello, world"),
},
templ.Attributes{
"onmouseleave": BasicScript("mouse left"),
"href": "/path/to/resource",
})

diff, err := htmldiff.Diff(component, expected)
if err != nil {
Expand Down
14 changes: 9 additions & 5 deletions generator/test-spread-attributes/template.templ
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package testspreadattributes

templ BasicTemplate(spread templ.Attributes) {
script BasicScript(message string) {
alert(message);
}

templ BasicTemplate(spread templ.Attributes, otherSpread templ.Attributes) {
<div>
<a { spread... }>text</a>
<div
<a { spread... } { otherSpread... }>text</a>
<button onclick={BasicScript("you clicked on me")}
if true {
{ spread... }
}
>text2</div>
>text2</button>
<div
if false {
{ spread... }
}
>text3</div>
</div>
}
}
55 changes: 51 additions & 4 deletions generator/test-spread-attributes/template_templ.go

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

18 changes: 18 additions & 0 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,10 @@ func RenderAttributes(ctx context.Context, w io.Writer, attributes Attributes) (
return err
}
}
case ComponentScript:
if err = writeStrings(w, ` `, EscapeString(key), `="`, value.Call, `"`); err != nil {
return err
}
}
}
return nil
Expand Down Expand Up @@ -691,6 +695,20 @@ func (c ComponentScript) Render(ctx context.Context, w io.Writer) error {

// RenderScriptItems renders a <script> element, if the script has not already been rendered.
func RenderScriptItems(ctx context.Context, w io.Writer, scripts ...ComponentScript) (err error) {
return RenderScriptItemsWithSpread(ctx, w, nil, scripts...)
Copy link
Contributor Author

@alehechka alehechka Mar 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mainly only went this route to keep RenderScriptItems unchanged to avoid a breaking change. But if that's not a concern we can just add the extra parameter to this function instead.

}

// RenderScriptItemsWithSpread renders a <script> element, if the script has not already been rendered.
// Includes support for scripts passed from spread attributes { attrs... }.
func RenderScriptItemsWithSpread(ctx context.Context, w io.Writer, attrSlice []Attributes, scripts ...ComponentScript) (err error) {
for _, attrs := range attrSlice {
for _, attr := range attrs {
if script, ok := attr.(ComponentScript); ok {
scripts = append(scripts, script)
}
}
}

if len(scripts) == 0 {
return nil
}
Expand Down