feat(loops): let a loop carry custom HTML attributes - #350
Open
mostafasadeghidev wants to merge 1 commit into
Open
feat(loops): let a loop carry custom HTML attributes#350mostafasadeghidev wants to merge 1 commit into
mostafasadeghidev wants to merge 1 commit into
Conversation
`base.loop` emits a real wrapper element with an author-selectable tag —
it even shares the tag controls with `base.container` — but it was the
one structural module that could not carry a single attribute. Container,
text, image, link, button, and the form modules all take `htmlAttributes`;
the loop's props schema had no such field and `renderLoop()` built the
wrapper's attributes from scratch.
A repeated list is precisely the element that needs addressing:
- `role="list"` / `aria-label` for assistive technology, which matters
more here than elsewhere because the loop can already be authored as
a `<ul>` or `<nav>`;
- a `data-*` hook for a carousel, filter, lightbox, or marquee script
that has to find the collection wrapper;
- an `id` for an anchor link into a section.
None of those were expressible, and the workaround — wrapping the loop in
a container just to hold an attribute — puts an element in the published
HTML that has no reason to exist.
The editing UI needed no change: the Properties panel's Attributes tab is
already generic over the selected node. This declares the prop, emits it
in `renderLoop()`, and mirrors it in `LoopEditor` so the canvas DOM keeps
matching the published DOM.
Values go through the shared sanitiser, which reserves the
`data-instatic-*` and `data-canvas-*` prefixes — the loop's pagination and
hole bookkeeping cannot be redirected from the attributes panel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mostafasadeghidev
marked this pull request as ready for review
August 7, 2026 00:03
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.
What
base.loopcan now carryhtmlAttributes, like the other structural modules.Why
The loop emits a real wrapper element with an author-selectable tag — its own doc comment says the tag controls are "the same controls as
base.container" — but it was the one structural module that could not carry a single attribute:htmlAttributesLoopPropsSchemahad no such field, andrenderLoop()built the wrapper's attribute string from scratch (data-instatic-loop*only).A repeated list is precisely the element that needs addressing:
role="list"/aria-label— this matters more on a loop than elsewhere, because the loop can already be authored as a<ul>or<nav>and there was no way to label it.data-*hook for a carousel, filter, lightbox, or marquee script that has to find the collection wrapper. This is how essentially every third-party list library binds.idfor an anchor link into a section.The workaround — wrapping the loop in a container purely to hold an attribute — puts an element in the published HTML that has no reason to be there, which is the opposite of what this project ships.
I hit it importing a site whose CMS list was animated by a script bound to the list container. The list rendered perfectly; the attribute the script looks for was gone, so the animation silently never ran.
How
Two-line contract, three files:
LoopPropsSchemagainshtmlAttributes, using the sameType.Record(…, HtmlAttributesPropSchemaOptions)declaration asbase.container.renderLoop()appendshtmlAttributesAttr(props.htmlAttributes)after its own bookkeeping attributes.LoopEditorspreadshtmlAttributesForReact(...)onto the canvas element, keeping the canvas DOM identical to the published DOM — the reason that component already mirrorsdata-instatic-loopby hand.No editor UI change was needed. The Properties panel's Attributes tab reads
selectedNode.props.htmlAttributesgenerically, so it already worked for loops — the value just had nowhere to live and nowhere to go.Safety is the shared sanitiser's, unchanged: names are normalised, event handlers and
javascript:URLs are dropped, values are escaped, and the reserveddata-instatic-*/data-canvas-*prefixes mean the loop's pagination and hole bookkeeping cannot be redirected from the attributes panel. There is a test for that last one.User impact
Additive. A loop with no attributes set emits byte-identical HTML to before (covered by a test).
Verification
Five new tests in
src/__tests__/publisher/loopRender.test.ts: attributes reach the wrapper, the reserved prefix holds, sanitiser rejections are dropped, values are escaped, and an unset bag changes nothing.Note:
bun test src/__tests__/architecture/reports 8 failures on this machine — the same 8 on unmodifiedorigin/main, so they are not from this change.Related, but deliberately separate — different reason, different files:
#348 is about which rows a loop selects; this is about the markup it emits. Both touch loops, so whichever lands second may need a trivial rebase.