Skip to content

feat(loops): let a loop carry custom HTML attributes - #350

Open
mostafasadeghidev wants to merge 1 commit into
CoreBunch:mainfrom
mostafasadeghidev:feat/loop-html-attributes
Open

feat(loops): let a loop carry custom HTML attributes#350
mostafasadeghidev wants to merge 1 commit into
CoreBunch:mainfrom
mostafasadeghidev:feat/loop-html-attributes

Conversation

@mostafasadeghidev

Copy link
Copy Markdown

What

base.loop can now carry htmlAttributes, 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:

Module htmlAttributes
container, text, image, link, button, forms
loop

LoopPropsSchema had no such field, and renderLoop() 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.
  • A 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.
  • An id for 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:

  • LoopPropsSchema gains htmlAttributes, using the same Type.Record(…, HtmlAttributesPropSchemaOptions) declaration as base.container.
  • renderLoop() appends htmlAttributesAttr(props.htmlAttributes) after its own bookkeeping attributes.
  • LoopEditor spreads htmlAttributesForReact(...) onto the canvas element, keeping the canvas DOM identical to the published DOM — the reason that component already mirrors data-instatic-loop by hand.

No editor UI change was needed. The Properties panel's Attributes tab reads selectedNode.props.htmlAttributes generically, 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 reserved data-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

bun test src/__tests__/publisher/    # 417 pass
bun run build
bun run lint

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 unmodified origin/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.

`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
mostafasadeghidev marked this pull request as ready for review August 7, 2026 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant