<ui-selected> detached clone deterministically reports hasAttribute TypeError
Nobody has claimed this yet.
- Dominant language
- Blade
- Stars
- 977
- Forks
- 112
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 21
Description
Flux version
v2.19.0, still present in v2.20.0
Livewire version
v4.4.4
Tailwind version
v4.3.3
Browser and Operating System
Chromium 153 and WebKit 26.6 (Playwright) on Linux
What is the problem?
Constructing a detached clone of <ui-selected> throws an uncaught error in both Chromium and WebKit.
UISelected.boot() runs during custom-element construction:
this.picker = this.closest("ui-select,ui-pillbox");
this.multiple = this.picker.hasAttribute("multiple");
cloneNode(true) upgrades the cloned custom element while it is still detached, so closest() returns null and hasAttribute() throws.
This is a follow-up to #2605, which was closed because the application-level reproduction could not be reproduced. The reproduction below needs no Laravel rendering, Livewire timing or application code. It runs the JavaScript shipped in the Flux package directly.
The same unguarded code is in every v2.20.0 runtime bundle: flux/dist/flux.min.js, flux/dist/flux-lite.min.js, flux-pro/dist/flux.js and flux-pro/dist/flux.module.js.
Fastest manual reproduction
Render any page containing a listbox select:
<flux:select variant="listbox">
<flux:select.option value="a">Option A</flux:select.option>
</flux:select>
Open DevTools and run:
document.querySelector('ui-selected').cloneNode(true)
Chromium:
Uncaught TypeError: Cannot read properties of null (reading 'hasAttribute')
WebKit:
TypeError: null is not an object (evaluating 'this.picker.hasAttribute')
Standalone automated reproduction
From the root of a Laravel application with Flux installed:
npm install --no-save playwright
npx playwright install chromium webkit
node flux-selected-repro.mjs
flux-selected-repro.mjs:
import { resolve } from 'node:path';
import { chromium, webkit } from 'playwright';
const fluxRuntime = resolve('vendor/livewire/flux/dist/flux.min.js');
for (const browserType of [chromium, webkit]) {
const browser = await browserType.launch({ headless: true });
const page = await browser.newPage();
await page.setContent('<ui-select><ui-selected></ui-selected></ui-select>');
await page.addScriptTag({ path: fluxRuntime });
await page.waitForFunction(() => customElements.get('ui-selected'));
const pageError = page.waitForEvent('pageerror');
const clone = await page.evaluate(() => {
const clone = document.querySelector('ui-selected').cloneNode(true);
return {
detached: clone.parentElement === null,
picker: clone.picker ?? null,
};
});
const error = await pageError;
console.log({
browser: browserType.name(),
browserVersion: browser.version(),
clone,
error: error.message,
});
await browser.close();
}
Output:
{
browser: 'chromium',
browserVersion: '153.0.8010.12',
clone: { detached: true, picker: null },
error: "Cannot read properties of null (reading 'hasAttribute')"
}
{
browser: 'webkit',
browserVersion: '26.6',
clone: { detached: true, picker: null },
error: "null is not an object (evaluating 'this.picker.hasAttribute')"
}
How it shows up in a real app
We hit this with two dependent listbox selects, where changing the first (wire:model.live) re-renders the second. Livewire's DOM update clones a subtree containing <ui-selected>, WebKit reports the error above, and the dependent select is left unusable.
Suggested fix
Tolerate detached construction in boot(), and resolve the picker again in mount() once the element is connected:
boot() {
this.picker = this.closest("ui-select,ui-pillbox");
this.multiple = this.picker?.hasAttribute("multiple") ?? false;
}
mount() {
this.picker ??= this.closest("ui-select,ui-pillbox");
if (!this.picker) return;
this.multiple = this.picker.hasAttribute("multiple");
// Existing mount logic...
}
We have been running this patch against the minified runtime. With it, the dependent select works, no error is reported, and connected single- and multiple-select clones pick up the correct owning picker and multiple state in Chromium and WebKit.
How do you expect it to work?
<ui-selected> should survive the detached construction that native cloneNode(true) performs without an uncaught exception. Once the clone is connected under <ui-select> or <ui-pillbox>, mount() should resolve that owner and initialize single- or multiple-selection state correctly.
Please confirm
- I have provided easy and step-by-step instructions to reproduce the bug.
- I have provided code samples as text and not images.
- I understand my bug report will be closed if I have not met the criteria above.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the UISelected boot and mount logic described in flux/dist/flux.min.js, flux/dist/flux-lite.min.js, flux-pro/dist/flux.js, and flux-pro/dist/flux.module.js. Run the standalone flux-selected-repro.mjs reproduction in Chromium and WebKit, then verify that detached cloning produces no page error and connected clones resolve their picker and multiple state correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, playwright
- Domain
- frontend, testing-qa
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100