Repository: a-h/templ (Go)

Issue:
Preserve order of `data-` attributes when using `templ.Attributes`
When passing `templ.Attributes` to a component (templUI in this case), the attributes appear to be added in alphabetical order, rather than insert order.

For example:

```templ
@alert.Alert(alert.Props{ID: id, Attributes: templ.Attributes{
	"data-signals": "{showAlert: true}",
	"data-show":    "$showAlert",
}}) {Some alert text.}
```

Will end up with `data-show` being added before `data-signals` (which causes an error in my app).

According to https://html.spec.whatwg.org/#embedding-custom-non-visible-data-with-the-data-*-attributes , 

> To get a DOMStringMap's name-value pairs, run the following algorithm:
> 
> Let list be an empty list of name-value pairs.
> 
> For each content attribute on the [DOMStringMap](https://html.spec.whatwg.org/#domstringmap)'s  [associated element](https://html.spec.whatwg.org/#concept-domstringmap-element) whose first five characters are the string "data-" and whose remaining characters (if any) do not include any [ASCII upper alphas](https://infra.spec.whatwg.org/#ascii-upper-alpha), **in the order that those attributes are listed in the element's [attribute list](https://dom.spec.whatwg.org/#concept-element-attribute)**, add a name-value pair to list whose name is the attribute's name with the first five characters removed and whose value is the  attribute's value.

The key bit being that the attributes are available to JS "in the order that those attributes are listed in the element's attribute list".

(Appreciate that this one is a tricky one, given that Attributes is a map under the covers.)



Fix (patch):
diff --git a/runtime.go b/runtime.go
index 94a743393..719481eab 100644
--- a/runtime.go
+++ b/runtime.go
@@ -413,19 +413,38 @@ func SanitizeCSS[T ~string](property string, value T) SafeCSS {
 	return SafeCSS(p + ":" + v + ";")
 }
 
+type Attributer interface {
+	Items() []KeyValue[string, any]
+}
+
 // Attributes is an alias to map[string]any made for spread attributes.
 type Attributes map[string]any
 
-// sortedKeys returns the keys of a map in sorted order.
-func sortedKeys(m map[string]any) (keys []string) {
-	keys = make([]string, len(m))
-	var i int
-	for k := range m {
-		keys[i] = k
+var _ Attributer = Attributes{}
+
+// Returns the items of the attributes map in key sorted order.
+func (a Attributes) Items() []KeyValue[string, any] {
+	var (
+		items = make([]KeyValue[string, any], len(a))
+		i     int
+	)
+	for k, v := range a {
+		items[i] = KeyValue[string, any]{Key: k, Value: v}
 		i++
 	}
-	sort.Strings(keys)
-	return keys
+	sort.Slice(items, func(i, j int) bool {
+		return items[i].Key < items[j].Key
+	})
+	return items
+}
+
+// OrderedAttributes stores attributes in order of insertion.
+type OrderedAttributes []KeyValue[string, any]
+
+var _ Attributer = OrderedAttributes{}
+
+func (a OrderedAttributes) Items() []KeyValue[string, any] {
+	return a
 }
 
 func writeStrings(w io.Writer, ss ...string) (err error) {
@@ -437,9 +456,10 @@ func writeStrings(w io.Writer, ss ...string) (err error) {
 	return nil
 }
 
-func RenderAttributes(ctx context.Context, w io.Writer, attributes Attributes) (err error) {
-	for _, key := range sortedKeys(attributes) {
-		value := attributes[key]
+func RenderAttributes(ctx context.Context, w io.Writer, attributes Attributer) (err error) {
+	for _, item := range attributes.Items() {
+		key := item.Key
+		value := item.Value
 		switch value := value.(type) {
 		case string:
 			if err = writeStrings(w, ` `, EscapeString(key), `="`, EscapeString(value), `"`); err != nil {