Open Closed

LeptonX strands a dead combobox whenever Blazor removes a <select> #10887


User avatar
0
rcalv002 created

LeptonX's FormSelect replaces every select.form-select with markup of its own

<div class="custom-select-wrapper" data-lpx-bound="true">
  <div class="custom-select-display form-select" role="combobox" aria-labelledby="{select id}">…</div>
  <select …>  <!-- Blazor owns only this -->
</div>

The wrapper is created by JS and is not part of Blazor's render tree. Its observer only ever reacts to additions:

setupMutationObserver() {
  observer = new MutationObserver(e => e.forEach(t => {
    …
    t.addedNodes.forEach(n => { … c.processSelect(a) })   // added only — no removedNodes branch
  }))
  observer.observe(document.body, { childList: true, subtree: true, … })
}

and there is no dispose/destroy/cleanup on the class (Object.getOwnPropertyNames(leptonx.FormSelect) has none).

So when Blazor removes a <select>, only the <select> goes. LeptonX's fully-rendered combobox stays behind — visible, clickable, with an aria-labelledby pointing at an id that no longer exists — and the next render binds the new select and builds another wrapper beside it.

Measured live: orphan wrappers climbed 4 → 5 → 6 → 7 while clicking between columns, and never dropped.

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

1 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • This is a known limitation/bug pattern of LeptonX’s FormSelect enhancement: it builds a non-Blazor-owned wrapper (.custom-select-wrapper) around the native <select> and (as you observed) it doesn’t reliably clean up when the underlying <select> is removed/re-rendered by a SPA renderer (like Blazor). The result is exactly what you measured: orphaned wrappers accumulating and duplicated/invalid combobox UI.

    • Workaround (client-side cleanup on removal): add your own DOM-removal hook that deletes the LeptonX wrapper when the underlying <select> leaves the DOM.

      • If you’re on ABP’s MVC/Razor Pages UI you can use ABP’s DOM API abp.dom.onNodeRemoved(...) (it’s based on MutationObserver) and remove .custom-select-wrapper nodes that no longer contain a <select> (or whose aria-labelledby target is missing).
      • For Blazor, the simplest robust approach is the same concept: observe removals and delete wrappers that have lost their <select>.

      Example “orphan wrapper” cleanup logic (plain JS; safe to run globally):

    // Run once on app startup
      const cleanupOrphanLeptonSelectWrappers = () => {
        document.querySelectorAll(".custom-select-wrapper[data-lpx-bound='true']").forEach(w => {
          const select = w.querySelector("select");
          if (!select) {
            w.remove();
            return;
          }
    
          // Optional: also remove if aria-labelledby points to missing element
          const display = w.querySelector(".custom-select-display[aria-labelledby]");
          if (display) {
            const id = display.getAttribute("aria-labelledby");
            if (id && !document.getElementById(id)) {
              w.remove();
            }
          }
        });
      };
    
      const obs = new MutationObserver(muts => {
        // If anything removed, run cleanup
        if (muts.some(m => m.removedNodes && m.removedNodes.length)) {
          cleanupOrphanLeptonSelectWrappers();
        }
      });
    
      obs.observe(document.body, { childList: true, subtree: true });
    
    • Workaround (avoid enhancement for Blazor-owned selects):

      • Don’t use the CSS class that LeptonX targets (you mentioned select.form-select). If you can render selects without form-select, LeptonX won’t wrap them.
      • Or replace the control with a Blazor component/select implementation that doesn’t rely on LeptonX’s overlay.
    • If you need a forced rebind/rebuild (when Blazor reuses DOM in-place): your “tear down wrapper then call leptonx.FormSelect.processSelect(select)” approach is consistent with the other reported LeptonX stale-wrapper issues. In practice:

      1. remove existing .custom-select-wrapper containing the select
      2. re-run processSelect(select) after the render cycle

      That avoids duplication and stale markup.

    • Product fix: there is no public/verified documentation for a dispose/destroy API on leptonx.FormSelect in your version, so a theme-side fix would need to add a removed-nodes path (or explicit destroy) in LeptonX.

    Sources (verified):

    • https://abp.io/docs/10.5/framework/ui/mvc-razor-pages/javascript-api/dom

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 16, 2026, 14:50
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.