Iterating a slot and wrapping components in markup causes front-end error

Created on 24 March 2025, 2 months ago

Overview

Iterating a slot's components and wrapping them in markup will cause XB to have a front-end rendering error (but the preview works fine).

Why would someone want to do this?

If a design system's style follows follows BEM convention, it's sometimes preferable to wrap child elements containing unknown other components in something that's addressable by the current component. Think of grids, lists, etc.

Copy/paste example of that doesn't work

example.component.yml

'$schema': 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json'
name: Example
status: stable
group: Molecule/Example
description: A simple example that shows wrapping slot components in markup will break things
props:
  dummy:
    title: Dummy
    required: false
slots:
  items:
    title: Items

example.twig

<div class="example">
  {% for item in items %}
    <div class="example__item">
      {{ item }}
    </div>
  {% endfor %}
</div>

A not very helpful stack trace

index.js?v=1:150 TypeError: Cannot read properties of undefined (reading 'element')
    at index.js?v=1:235:31377
    at H5 (index.js?v=1:40:24296)
    at Ug (index.js?v=1:40:42451)
    at index.js?v=1:40:40763
    at k (index.js?v=1:25:1600)
    at MessagePort.j (index.js?v=1:25:1971)

A very dirty workaround

By using #prefix and #suffix, one can seemingly work around this. See the altered twig:

<div class="example">
  {% for item in items %}
    {% if item is iterable %}
      {% set item = item|merge({
        '#prefix': item['#prefix'] ~ '<div class="example__item">',
        '#suffix': '</div>' ~ item['#suffix'],
      }) %}
    {% endif %}
    {{ item }}
  {% endfor %}
</div>

Is this something that should be supported?

Proposed resolution

TBD

User interface changes

TBD

🐛 Bug report
Status

Active

Version

0.0

Component

Page builder

Created by

🇺🇸United States luke.leber Pennsylvania

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @luke.leber
  • 🇺🇸United States luke.leber Pennsylvania
  • Assigned to lauriii
  • 🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

    I doubt we can fix this 😬🙈 Except maybe somehow on the client side thanks to the HTML comments we're injecting? 🤔

    This seems very uncommon. So deprioritizing.

    @lauriii, thoughts? Product impact: SDCs using this (fairly advanced) pattern do not work at all in XB's preview.

    From a technical POV, my preferred solution would be to to detect this and disable such SDCs automatically. But then we'd need to parse the Twig markup of each SDC that has slots. That would slow down component discovery significantly. So I'd recommend against that. Which means this IMHO should remain an open low-priority bug. (Sorry, @luke.leber, but there's only so much time in a day — and this AFAICT would impact a tiny fraction of people.)

  • 🇺🇸United States effulgentsia

    @luke.leber: I'm surprised that the dirty workaround in the issue summary works. Are you sure that it does? As in, can you drag and drop into that slot? I think there are two potential issues here...

    1. The slot itself (not just its children) needs to be rendered somewhere. In other words, the issue summary's workaround would make sense to me if it ended with {% endfor %}{{ items }}</div> instead of {{ item }}{% endfor %}</div>.
    2. There are markers (HTML comments) in each item's #prefix and #suffix and those need to be direct children of the slot in the rendered output. For this, the dirty workaround in the issue summary addresses that by putting the desired wrapping element "inside" the existing #prefix and #suffix rather that "outside" of it.
  • 🇺🇸United States effulgentsia

    I wonder if we could solve this use case by adding a |wrapChildren filter. E.g., the example in the issue summary could then be:

    <div class="example">
      {{ items|wrapChildren(item => '<div class="example__item">' ~ item ~ '</div>') }}
    </div>
    

    We could implement the wrapChildren filter to act on each item before applying the item's #prefix and #suffix, and the above ensures that items itself is rendered and not just iterated.

    @luke.leber: Would something like this meet the use cases you're aware of, or is there more complex logic than what Twig allows inside an arrow function that you'd need?

  • 🇺🇸United States luke.leber Pennsylvania

    In all honesty, I haven't had any sponsored time to spend on XB since Atlanta.

    I would be curious to hear what Pierre thinks since his SDC advice of iterating slots (versus pursuing dynamic slots is what led me to discover the hacky workaround.

    It does seem a very, very fragile state of affairs that iterating a slot's components and injecting some markup can cause XB's front-end to fail by using only well supported twig. I'm assuming that a simple preprocess could do the same.

    I do think that this quirk will be fairly commonplace to find in the wild.

  • 🇫🇮Finland lauriii Finland

    One example use case we should think about is lists. If you need a list component that can include links, text, and images, how would we build that?

    I like the idea of dynamic slots but AFAIK neither Twig or React really supports this. #5 seems like it could potentially work for XB but wouldn't really work outside of XB. Or can we think of a syntax that would allow us for other Twig template to pass multiple slot values?

    I don't think this issue at least justifies parsing Twig markup. It seems if anything, we should make the client a bit more resilient.

  • 🇺🇸United States effulgentsia

    Although I don't think this needs to be figured out before beta, I think before tagging a stable release we should come to a decision on whether a slot should be thought of as a black box, or if it's legitimate for SDCs to treat a slot as an array of components and be able to decorate them in some way.

Production build 0.71.5 2024