livewire / livewire/flux

<ui-selected> detached clone deterministically reports hasAttribute TypeError

Open Beginner friendly
#2,825 0 comments 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.