Repository navigation
feat: Key, KeyTarget and KeyOf for one instance of a keyed component - #41
Merged
Merged
Conversation
A component rendered once per item (a table row, a card) had one id for
every instance, so apps hand-built "@{Page.Row |> id}-@{key}" ids. A
request targeting one of them matched no component and rendered Page
into the row, and Props had no way to learn which item was wanted.
Key(ctx, v, key) and KeyTarget generate the id of one instance: the
component's id as ID generates it, "--", and the key with every byte
outside [A-Za-z0-9-] written as _XX ("_" included), so the id needs no
CSS escaping and the key round-trips; only canonical escapes decode, so
a key has one id. A plain string, which names no component, and an
empty key are errors. *StructPages gets Key and KeyTarget too, and the
repo's gsx.toml registers them as the key and keytarget filters.
Generated ids never contain "--", so the first one ends the component
id. A function's id took its prefix from the last import path element,
which can hold hyphens of its own ("my-Lib" kebab-cased to "my--lib");
idSegment now collapses hyphen runs and trims the ends, which changes
the ids of functions in such packages only.
KeyOf(t) returns the key a RenderTarget carries. The interface is
unchanged: the built-in targets hold the key in a field and gain no
method, so Props, which takes the target at its dynamic type, fills its
parameters as before; a custom target opts in with a Key() string
method. Like React's key, the key is the item's identity; unlike it,
Props can read it back, so it is client input to authorise.
Both htmx selectors now go through htmxTarget. A keyed id matches only
exactly: its component id must be a generated id of the served page (the
target Is it), or of another page's component or a standalone function
(unmatched, but Is it). Keyed ids skip the loose name and suffix passes,
which would match a key ending in a component's name to that component.
Unkeyed ids match as before.
structpages-lint checks Key/KeyTarget like ID/IDTarget: page resolution
(idfor) and borrowed regions (region).
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #41 +/- ##
==========================================
+ Coverage 95.45% 95.72% +0.26%
==========================================
Files 18 20 +2
Lines 1759 1870 +111
==========================================
+ Hits 1679 1790 +111
Misses 48 48
Partials 32 32 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Oct 29, 2026
jackielii
added a commit
that referenced
this pull request
Oct 29, 2026
camelToKebab lowercased every rune with r | 0x20, which is only right for
ASCII capitals. An underscore (0x5F) became DEL (0x7F), so a page field
or component named Row_Edit got the id "row\x7f-edit", and letters
outside Latin-1 were garbled ("Größe" became "gröÿe"). It also compared
bytes, not runes, around multi-byte letters.
It now walks runes and lowercases with unicode.ToLower, finding word
starts with unicode.IsUpper/IsLower. Underscores and hyphens separate
words: "Row_Edit" and "row_edit" become "row-edit", and a hyphenated
import path element such as "ui-Kit" becomes "ui-kit" rather than
"ui--kit". Hyphens never repeat or start or end the result.
Ids of names made of ASCII letters and digits are unchanged, checked
against the old function over 100,000 random names. Ids change only for
names with underscores, non-ASCII letters, or a function's package path
element with leading, trailing or doubled hyphens or a hyphen before a
capital. Two names that now kebab-case alike on one tree (RowEdit and
Row_Edit) are reported by Mount's id uniqueness check.
idSegment, which #41 added to collapse hyphen runs after camelToKebab,
is now camelToKebab itself and is removed.
jackielii
added a commit
that referenced
this pull request
Oct 29, 2026
camelToKebab lowercased every rune with r | 0x20, which is only right for
ASCII capitals. An underscore (0x5F) became DEL (0x7F), so a page field
or component named Row_Edit got the id "row\x7f-edit", and letters
outside Latin-1 were garbled ("Größe" became "gröÿe"). It also compared
bytes, not runes, around multi-byte letters.
It now walks runes and lowercases with unicode.ToLower, finding word
starts with unicode.IsUpper/IsLower. Underscores and hyphens separate
words: "Row_Edit" and "row_edit" become "row-edit", and a hyphenated
import path element such as "ui-Kit" becomes "ui-kit" rather than
"ui--kit". Hyphens never repeat or start or end the result.
Ids of names made of ASCII letters and digits are unchanged, checked
against the old function over 100,000 random names. Ids change only for
names with underscores, non-ASCII letters, or a function's package path
element with leading, trailing or doubled hyphens or a hyphen before a
capital. Two names that now kebab-case alike on one tree (RowEdit and
Row_Edit) are reported by Mount's id uniqueness check.
idSegment, which #41 added to collapse hyphen runs after camelToKebab,
is now camelToKebab itself and is removed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A component rendered once per item (a table row, a card) had one id for every instance. Apps hand-built
@{Page.Row |> id}-@{key}ids (EOS has 14). A request targeting one instance matched no component and swapped the wholePageinto the row, and Props had no way to tell which item was wanted, so each keyed component needed its own route.API (additive)
Like React's
key, the key is the item's identity. Unlike React's, Props can read it back.Behaviour
--, then the key. Bytes outside[A-Za-z0-9-]are written as_XX,_included. Only canonical escapes decode, so each key has exactly one id.htmxTarget. A keyed id matches only exactly: the part before--must be a generated id.Isthat component and carries the key.Isthat component and carries the key.KeyOf: theRenderTargetinterface is unchanged. The built-in targets hold the key in a field and gain no method, so Props' dynamic-type injection fills parameters as before. A custom target opts in withKey() string.Compatibility
--, BEM-stylecard--activeincluded. They now match only exactly, so one that used to match a component loosely rendersPage.my-Libkebab-cased tomy--lib), the newidSegmentcollapses the runs and trims the ends. This changes the ids of functions in such packages only, so that generated ids never contain--.Lint
structpages-lintchecksKey/KeyTarget(package functions,sp.methods, and thekey/keytargetfilters) the same way it checksID/IDTarget, for bothidforandregion.Testing
--,-,_and non-ASCII bytes.--.Key() string.tools/lint: addsKeyfixtures totestdata/region(gsx) andtestdata/render.TestRegion_Integrationwas already failing onmainbecause 8.templfindings are missing. It fails on exactly the same 8 here, and all the new gsx findings are present.Key()interfaces;--in function ids from hyphenated package paths;Release
After merge, release
v0.11.0andtools/lint/v0.1.19.