Skip to content

<ui-selected> detached clone deterministically reports hasAttribute TypeError #2825

Description

@cyppe

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions