Skip to content

fix(core): prevent SlotController.getSlotted() returning empty array prior to initialization#3093

Merged
bennypowers merged 2 commits intomainfrom
fix/slot-controller-get-slotted
Apr 15, 2026
Merged

fix(core): prevent SlotController.getSlotted() returning empty array prior to initialization#3093
bennypowers merged 2 commits intomainfrom
fix/slot-controller-get-slotted

Conversation

@bennypowers
Copy link
Copy Markdown
Member

Summary

Fixed getSlotted() returning empty arrays when called before async
slot record initialization completes. Falls back to querying slot
elements directly when records aren't yet populated.

Closes #2946

Test plan

  • Existing slot-controller tests pass
  • Verify getSlotted() returns correct elements in downstream consumers

Add fallback in getSlotted() to query slot elements directly when
async slot record initialization hasn't completed yet.

Closes #2946

Assisted-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 15, 2026

🦋 Changeset detected

Latest commit: e90ca21

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@patternfly/pfe-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@netlify
Copy link
Copy Markdown

netlify bot commented Apr 15, 2026

Deploy Preview for patternfly-elements ready!

Name Link
🔨 Latest commit e90ca21
🔍 Latest deploy log https://app.netlify.com/projects/patternfly-elements/deploys/69dfa288bae1ae00091900de
😎 Deploy Preview https://deploy-preview-3093--patternfly-elements.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions bot added the AT passed Automated testing has passed label Apr 15, 2026
@github-actions
Copy link
Copy Markdown
Contributor

SSR Test Run for 7525cea: Report

@bennypowers bennypowers enabled auto-merge (squash) April 15, 2026 13:13
@bennypowers bennypowers changed the title fix(core): SlotController.getSlotted() returning empty array fix(core): prevent SlotController.getSlotted() returning empty array prior to initialization Apr 15, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 15, 2026

✅ Commitlint tests passed!

More Info
{
  "valid": true,
  "errors": [],
  "warnings": [],
  "input": "fix(core): prevent SlotController.getSlotted() returning empty array prior to initialization"
}

@zeroedin
Copy link
Copy Markdown
Collaborator

zeroedin commented Apr 15, 2026

Alternative approach:

synchronous hostUpdated() instead of async fallback

The current fix adds a fallback in getSlotted() to query slot elements directly when #slotRecords hasn't been populated yet. This addresses the symptom but leaves the root cause: hostConnected and #initSlotMap are async, creating a timing gap where no data source exists.

The gap also affects hasSlotted() and isEmpty(), which aren't addressed here.

Why hostConnected is async

Both hostConnected and #initSlotMap await host.updateComplete just to ensure the shadow DOM exists so they can query for elements. But Lit already provides a synchronous hook that guarantees this: hostUpdated(), which runs after every render when the DOM has been committed.

Suggested change

Make hostConnected synchronous (just sets up the MutationObserver), move record initialization to hostUpdated(), and have the MutationObserver trigger a re-render instead of calling #initSlotMap directly:

  hostConnected(): void {
    this.#mo.observe(this.host, { childList: true });
    this.#slotRecords.clear();
  }

  hostUpdated(): void {
    if (this.#slotRecords.size === 0) {
      for (let slotName of this.#slotNames.concat(Object.values(this.#deprecations))) {
        slotName ||= SlotController.default;
        const slot = this.#getSlotElement(slotName);
        if (slot) {
          this.#slotRecords.set(slotName, new SlotRecord(slot, slotName, this.host));
        }
      }
      if (this.#slotRecords.size > 0) {
        this.host.requestUpdate();
      }
    }
  }

The MutationObserver callback becomes:

#mo = new MutationObserver(() => this.host.requestUpdate());

And #initSlotMap can be removed entirely.

This also requires updating the SlotControllerPublicAPI declaration to match:

hostConnected?(): void;

Why this works

  • First render: Records empty, isEmpty() returns true (matches SSR server behavior). hostUpdated() populates records and calls requestUpdate().
  • Second render: Records exist, isEmpty()/getSlotted() read live DOM via SlotRecord getters, correct results. hostUpdated() sees records exist, no requestUpdate(), no infinite loop.
  • MutationObserver: Child changes trigger requestUpdate() → re-render → SlotRecord getters reflect new DOM state. No record rebuild needed since they query live DOM.
  • Reconnection: hostConnected clears records → next render → hostUpdated() repopulates.

What this fixes that the current PR doesn't

  • hasSlotted() and isEmpty() have the same timing gap, they also read from #slotRecords and return incorrect results before async init completes. The hostUpdated() approach fixes all three methods.
  • Eliminates the async architecture that caused the issue rather than working around it.

Trade-off

The two-render pattern on initial connection is inherent <slot> elements are created by render(), so the first render can't know about them. This was already the case with the async approach.

We can spin this off to another issue if we feel the need

@github-actions
Copy link
Copy Markdown
Contributor

SSR Test Run for 4c273cb: Report

Copy link
Copy Markdown
Collaborator

@zeroedin zeroedin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me as is, the alternative comment can be broken out into its own issue and is not a blocker to this PR. Tested artifact down stream in RHDS.

@bennypowers bennypowers merged commit 7e31427 into main Apr 15, 2026
15 checks passed
@bennypowers bennypowers deleted the fix/slot-controller-get-slotted branch April 15, 2026 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AT passed Automated testing has passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug]: SlotController issue in getSlotted() not returning elements as expected

2 participants