Skip to content

Commit 732862b

Browse files
authored
fix: add support for spreading non-string attribute values in RenderAttributes (#1213)
1 parent 554776e commit 732862b

File tree

5 files changed

+266
-25
lines changed

5 files changed

+266
-25
lines changed

.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.906
1+
0.3.908
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
<a bool-true complex-value="(1+2i)" float-value="3.14" int-value="42" int64-value="9223372036854775807" string-value="text" uint-value="100">text</a>
3+
<div bool-true complex-value="(1+2i)" float-value="3.14" int-value="42" int64-value="9223372036854775807" string-value="text" uint-value="100">text2</div>
4+
<div>text3</div>
5+
</div>

generator/test-spread-attributes/render_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,28 @@ func nilPtr[T any]() *T {
111111
func ptr[T any](x T) *T {
112112
return &x
113113
}
114+
115+
//go:embed expected_numeric_attributes.html
116+
var expectedNumericAttributes string
117+
118+
func TestNumericAttributeTypes(t *testing.T) {
119+
t.Parallel()
120+
component := BasicTemplate(templ.Attributes{
121+
"int-value": 42,
122+
"float-value": 3.14,
123+
"uint-value": uint(100),
124+
"int64-value": int64(9223372036854775807),
125+
"complex-value": complex(1, 2),
126+
"string-value": "text",
127+
"bool-true": true,
128+
"bool-false": false,
129+
})
130+
131+
diff, err := htmldiff.Diff(component, expectedNumericAttributes)
132+
if err != nil {
133+
t.Fatal(err)
134+
}
135+
if diff != "" {
136+
t.Error(diff)
137+
}
138+
}

runtime.go

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -466,46 +466,82 @@ func RenderAttributes(ctx context.Context, w io.Writer, attributes Attributer) (
466466
return err
467467
}
468468
case *string:
469-
if value != nil {
470-
if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(*value), `"`); err != nil {
471-
return err
472-
}
469+
if value == nil {
470+
continue
471+
}
472+
if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(*value), `"`); err != nil {
473+
return err
473474
}
474475
case bool:
475-
if value {
476-
if err = writeStrings(w, ` `, EscapeString(key)); err != nil {
477-
return err
478-
}
476+
if !value {
477+
continue
478+
}
479+
if err = writeStrings(w, ` `, EscapeString(key)); err != nil {
480+
return err
479481
}
480482
case *bool:
481-
if value != nil && *value {
482-
if err = writeStrings(w, ` `, EscapeString(key)); err != nil {
483-
return err
484-
}
483+
if value == nil || !*value {
484+
continue
485+
}
486+
if err = writeStrings(w, ` `, EscapeString(key)); err != nil {
487+
return err
488+
}
489+
case int, int8, int16, int32, int64,
490+
uint, uint8, uint16, uint32, uint64, uintptr,
491+
float32, float64, complex64, complex128:
492+
if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(fmt.Sprint(value)), `"`); err != nil {
493+
return err
494+
}
495+
case *int, *int8, *int16, *int32, *int64,
496+
*uint, *uint8, *uint16, *uint32, *uint64, *uintptr,
497+
*float32, *float64, *complex64, *complex128:
498+
value = ptrValue(value)
499+
if value == nil {
500+
continue
501+
}
502+
if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(fmt.Sprint(value)), `"`); err != nil {
503+
return err
485504
}
486505
case KeyValue[string, bool]:
487-
if value.Value {
488-
if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(value.Key), `"`); err != nil {
489-
return err
490-
}
506+
if !value.Value {
507+
continue
508+
}
509+
if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(value.Key), `"`); err != nil {
510+
return err
491511
}
492512
case KeyValue[bool, bool]:
493-
if value.Value && value.Key {
494-
if err = writeStrings(w, ` `, EscapeString(key)); err != nil {
495-
return err
496-
}
513+
if !value.Value || !value.Key {
514+
continue
515+
}
516+
if err = writeStrings(w, ` `, EscapeString(key)); err != nil {
517+
return err
497518
}
498519
case func() bool:
499-
if value() {
500-
if err = writeStrings(w, ` `, EscapeString(key)); err != nil {
501-
return err
502-
}
520+
if !value() {
521+
continue
522+
}
523+
if err = writeStrings(w, ` `, EscapeString(key)); err != nil {
524+
return err
503525
}
504526
}
505527
}
506528
return nil
507529
}
508530

531+
func ptrValue(v any) any {
532+
if v == nil {
533+
return nil
534+
}
535+
rv := reflect.ValueOf(v)
536+
if rv.Kind() != reflect.Ptr {
537+
return v
538+
}
539+
if rv.IsNil() {
540+
return nil
541+
}
542+
return rv.Elem().Interface()
543+
}
544+
509545
// Context.
510546

511547
type contextKeyType int

runtime_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,3 +612,178 @@ func TestNonce(t *testing.T) {
612612
}
613613
})
614614
}
615+
616+
func TestRenderAttributes(t *testing.T) {
617+
tests := []struct {
618+
name string
619+
attributes templ.Attributes
620+
expected string
621+
}{
622+
{
623+
name: "string attributes are rendered",
624+
attributes: templ.Attributes{
625+
"class": "test-class",
626+
"id": "test-id",
627+
},
628+
expected: ` class="test-class" id="test-id"`,
629+
},
630+
{
631+
name: "integer types are rendered as strings",
632+
attributes: templ.Attributes{
633+
"int": 42,
634+
"int8": int8(8),
635+
"int16": int16(16),
636+
"int32": int32(32),
637+
"int64": int64(64),
638+
},
639+
expected: ` int="42" int16="16" int32="32" int64="64" int8="8"`,
640+
},
641+
{
642+
name: "unsigned integer types are rendered as strings",
643+
attributes: templ.Attributes{
644+
"uint": uint(42),
645+
"uint8": uint8(8),
646+
"uint16": uint16(16),
647+
"uint32": uint32(32),
648+
"uint64": uint64(64),
649+
"uintptr": uintptr(100),
650+
},
651+
expected: ` uint="42" uint16="16" uint32="32" uint64="64" uint8="8" uintptr="100"`,
652+
},
653+
{
654+
name: "float types are rendered as strings",
655+
attributes: templ.Attributes{
656+
"float32": float32(3.14),
657+
"float64": float64(2.718),
658+
},
659+
expected: ` float32="3.14" float64="2.718"`,
660+
},
661+
{
662+
name: "complex types are rendered as strings",
663+
attributes: templ.Attributes{
664+
"complex64": complex64(1 + 2i),
665+
"complex128": complex128(3 + 4i),
666+
},
667+
expected: ` complex128="(3+4i)" complex64="(1+2i)"`,
668+
},
669+
{
670+
name: "boolean attributes are rendered correctly",
671+
attributes: templ.Attributes{
672+
"checked": true,
673+
"disabled": false,
674+
},
675+
expected: ` checked`,
676+
},
677+
{
678+
name: "mixed types are rendered correctly",
679+
attributes: templ.Attributes{
680+
"class": "button",
681+
"value": 42,
682+
"width": float64(100.5),
683+
"hidden": false,
684+
"active": true,
685+
},
686+
expected: ` active class="button" value="42" width="100.5"`,
687+
},
688+
{
689+
name: "nil pointer attributes are not rendered",
690+
attributes: templ.Attributes{
691+
"optional": (*string)(nil),
692+
"visible": (*bool)(nil),
693+
},
694+
expected: ``,
695+
},
696+
{
697+
name: "non-nil pointer attributes are rendered",
698+
attributes: templ.Attributes{
699+
"title": ptr("test title"),
700+
"enabled": ptr(true),
701+
},
702+
expected: ` enabled title="test title"`,
703+
},
704+
{
705+
name: "numeric pointer types are rendered as strings",
706+
attributes: templ.Attributes{
707+
"int-ptr": ptr(42),
708+
"int8-ptr": ptr(int8(8)),
709+
"int16-ptr": ptr(int16(16)),
710+
"int32-ptr": ptr(int32(32)),
711+
"int64-ptr": ptr(int64(64)),
712+
"uint-ptr": ptr(uint(42)),
713+
"uint8-ptr": ptr(uint8(8)),
714+
"uint16-ptr": ptr(uint16(16)),
715+
"uint32-ptr": ptr(uint32(32)),
716+
"uint64-ptr": ptr(uint64(64)),
717+
"uintptr-ptr": ptr(uintptr(100)),
718+
"float32-ptr": ptr(float32(3.14)),
719+
"float64-ptr": ptr(float64(2.718)),
720+
"complex64-ptr": ptr(complex64(1 + 2i)),
721+
"complex128-ptr": ptr(complex128(3 + 4i)),
722+
},
723+
expected: ` complex128-ptr="(3+4i)" complex64-ptr="(1+2i)" float32-ptr="3.14" float64-ptr="2.718" int-ptr="42" int16-ptr="16" int32-ptr="32" int64-ptr="64" int8-ptr="8" uint-ptr="42" uint16-ptr="16" uint32-ptr="32" uint64-ptr="64" uint8-ptr="8" uintptr-ptr="100"`,
724+
},
725+
{
726+
name: "nil numeric pointer attributes are not rendered",
727+
attributes: templ.Attributes{
728+
"int-ptr": (*int)(nil),
729+
"float32-ptr": (*float32)(nil),
730+
"complex64-ptr": (*complex64)(nil),
731+
},
732+
expected: ``,
733+
},
734+
{
735+
name: "KeyValue[string, bool] attributes are rendered correctly",
736+
attributes: templ.Attributes{
737+
"data-value": templ.KV("test-string", true),
738+
"data-hidden": templ.KV("ignored", false),
739+
},
740+
expected: ` data-value="test-string"`,
741+
},
742+
{
743+
name: "KeyValue[bool, bool] attributes are rendered correctly",
744+
attributes: templ.Attributes{
745+
"checked": templ.KV(true, true),
746+
"disabled": templ.KV(false, true),
747+
"hidden": templ.KV(true, false),
748+
},
749+
expected: ` checked`,
750+
},
751+
{
752+
name: "function bool attributes are rendered correctly",
753+
attributes: templ.Attributes{
754+
"enabled": func() bool { return true },
755+
"hidden": func() bool { return false },
756+
},
757+
expected: ` enabled`,
758+
},
759+
{
760+
name: "mixed KeyValue and function attributes",
761+
attributes: templ.Attributes{
762+
"data-name": templ.KV("value", true),
763+
"active": templ.KV(true, true),
764+
"dynamic": func() bool { return true },
765+
"ignored": templ.KV("ignored", false),
766+
},
767+
expected: ` active data-name="value" dynamic`,
768+
},
769+
}
770+
for _, tt := range tests {
771+
t.Run(tt.name, func(t *testing.T) {
772+
t.Parallel()
773+
var buf bytes.Buffer
774+
err := templ.RenderAttributes(context.Background(), &buf, tt.attributes)
775+
if err != nil {
776+
t.Fatalf("RenderAttributes failed: %v", err)
777+
}
778+
779+
actual := buf.String()
780+
if actual != tt.expected {
781+
t.Errorf("expected %q, got %q", tt.expected, actual)
782+
}
783+
})
784+
}
785+
}
786+
787+
func ptr[T any](x T) *T {
788+
return &x
789+
}

0 commit comments

Comments
 (0)