Skip to content

Commit

Permalink
feat: add spread attribute feature (#237)
Browse files Browse the repository at this point in the history
Co-authored-by: Joe Davidson <[email protected]>
Co-authored-by: Adrian Hesketh <[email protected]>
  • Loading branch information
3 people authored Dec 21, 2023
1 parent 83497bc commit 37f022a
Show file tree
Hide file tree
Showing 12 changed files with 476 additions and 1 deletion.
32 changes: 31 additions & 1 deletion docs/docs/03-syntax-and-usage/03-attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Use an `if` statement within a templ element to optionally add attributes to ele

```templ
templ component() {
<hr style="padding: 10px"
<hr style="padding: 10px"
if true {
class="itIsTrue"
}
Expand All @@ -63,6 +63,36 @@ templ component() {
<hr style="padding: 10px" class="itIsTrue" />
```

## Spread attributes

Use the `{ attrMap... }` syntax in the open tag of an element to append a dynamic map of attributes to the element's attributes.

It's possible to spread any variable of type `templ.Attributes`. `templ.Attributes` is a `map[string]any` type definition.

* If the value is a `string`, the attribute is added with the string value, e.g. `<div name="value">`.
* 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>`.

```templ
templ component(shouldBeUsed bool, attrs templ.Attributes) {
<p { attrs... }></p>
<hr
if shouldBeUsed {
{ attrs... }
}
/>
}
templ usage() {
@component(false, templ.Attributes{"data-testid": "paragraph"})
}
```

```html title="Output"
<p data-testid="paragraph">Text</p>
```

## URL attributes

The `<a>` element's `href` attribute is treated differently. templ expects you to provide a `templ.SafeURL` instead of a `string`.
Expand Down
23 changes: 23 additions & 0 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,27 @@ func (g *generator) writeExpressionAttribute(indentLevel int, elementName string
return nil
}

func (g *generator) writeSpreadAttributes(indentLevel int, attr parser.SpreadAttributes) (err error) {
// templ.RenderAttributes(ctx, w, spreadAttrs)
if _, err = g.w.WriteIndent(indentLevel, `templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, `); err != nil {
return err
}
// spreadAttrs
var r parser.Range
if r, err = g.w.Write(attr.Expression.Value); err != nil {
return err
}
g.sourceMap.Add(attr.Expression, r)
// )
if _, err = g.w.Write(")\n"); err != nil {
return err
}
if err = g.writeErrorHandler(indentLevel); err != nil {
return err
}
return nil
}

func (g *generator) writeConditionalAttribute(indentLevel int, elementName string, attr parser.ConditionalAttribute) (err error) {
// if
if _, err = g.w.WriteIndent(indentLevel, `if `); err != nil {
Expand Down Expand Up @@ -1196,6 +1217,8 @@ func (g *generator) writeElementAttributes(indentLevel int, name string, attrs [
err = g.writeBoolExpressionAttribute(indentLevel, attr)
case parser.ExpressionAttribute:
err = g.writeExpressionAttribute(indentLevel, name, attr)
case parser.SpreadAttributes:
err = g.writeSpreadAttributes(indentLevel, attr)
case parser.ConditionalAttribute:
err = g.writeConditionalAttribute(indentLevel, name, attr)
default:
Expand Down
11 changes: 11 additions & 0 deletions generator/test-spread-attributes/expected.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<div>
<a bool dateid="my-custom-id" hx-get="/page" id="test" nonshade optional-from-func-true text="lorem">
text
</a>
<div bool dateid="my-custom-id" hx-get="/page" id="test" nonshade optional-from-func-true text="lorem">
text2
</div>
<div>
text3
</div>
</div>
49 changes: 49 additions & 0 deletions generator/test-spread-attributes/render_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package testspreadattributes

import (
_ "embed"
"testing"

"github.com/a-h/templ"
"github.com/a-h/templ/generator/htmldiff"
)

//go:embed expected.html
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 },
})

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-spread-attributes/template.templ
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package testspreadattributes

templ BasicTemplate(spread templ.Attributes) {
<div>
<a { spread... }>text</a>
<div
if true {
{ spread... }
}
>text2</div>
<div
if false {
{ spread... }
}
>text3</div>
</div>
}
89 changes: 89 additions & 0 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.

45 changes: 45 additions & 0 deletions parser/v2/elementparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,48 @@ var expressionAttributeParser = parse.Func(func(pi *parse.Input) (attr Expressio
return attr, true, nil
})

var spreadAttributesParser = parse.Func(func(pi *parse.Input) (attr SpreadAttributes, ok bool, err error) {
start := pi.Index()

// Optional whitespace leader.
if _, ok, err = parse.OptionalWhitespace.Parse(pi); err != nil || !ok {
return
}

// Eat the first brace.
if _, ok, err = openBraceWithOptionalPadding.Parse(pi); err != nil ||
!ok {
pi.Seek(start)
return
}

// Expression.
if attr.Expression, ok, err = exp.Parse(pi); err != nil || !ok {
pi.Seek(start)
return
}

// Check if end of expression has "..." for spread.
if !strings.HasSuffix(attr.Expression.Value, "...") {
pi.Seek(start)
ok = false
return
}

// Remove extra spread characters from expression.
attr.Expression.Value = strings.TrimSuffix(attr.Expression.Value, "...")
attr.Expression.Range.To.Col -= 3
attr.Expression.Range.To.Index -= 3

// Eat the final brace.
if _, ok, err = closeBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
err = parse.Error("attribute spread expression: missing closing brace", pi.Position())
return
}

return attr, true, nil
})

// Attributes.
type attributeParser struct{}

Expand All @@ -276,6 +318,9 @@ func (attributeParser) Parse(in *parse.Input) (out Attribute, ok bool, err error
if out, ok, err = boolConstantAttributeParser.Parse(in); err != nil || ok {
return
}
if out, ok, err = spreadAttributesParser.Parse(in); err != nil || ok {
return
}
if out, ok, err = constantAttributeParser.Parse(in); err != nil || ok {
return
}
Expand Down
65 changes: 65 additions & 0 deletions parser/v2/elementparser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,28 @@ if test {
},
},
},
{
name: "spread attributes",
input: ` { spread... }"`,
parser: StripType(spreadAttributesParser),
expected: SpreadAttributes{
Expression{
Value: "spread",
Range: Range{
From: Position{
Index: 3,
Line: 0,
Col: 3,
},
To: Position{
Index: 9,
Line: 0,
Col: 9,
},
},
},
},
},
{
name: "constant attribute",
input: ` href="test"`,
Expand Down Expand Up @@ -455,6 +477,49 @@ func TestElementParser(t *testing.T) {
},
},
},
{
name: "element: self-closing with multiple spreads attributes",
input: `<a { firstSpread... } { children... }/>`,
expected: Element{
Name: "a",
Attributes: []Attribute{
SpreadAttributes{
Expression: Expression{
Value: "firstSpread",
Range: Range{
From: Position{
Index: 5,
Line: 0,
Col: 5,
},
To: Position{
Index: 16,
Line: 0,
Col: 16,
},
},
},
},
SpreadAttributes{
Expression: Expression{
Value: "children",
Range: Range{
From: Position{
Index: 24,
Line: 0,
Col: 24,
},
To: Position{
Index: 32,
Line: 0,
Col: 32,
},
},
},
},
},
},
},
{
name: "element: self-closing with multiple boolean attributes",
input: `<hr optionA optionB?={ true } optionC="other"/>`,
Expand Down
Loading

0 comments on commit 37f022a

Please sign in to comment.