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.
1 Answer(s)
-
0
This is a known limitation/bug pattern of LeptonX’s
FormSelectenhancement: 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 onMutationObserver) and remove.custom-select-wrappernodes that no longer contain a<select>(or whosearia-labelledbytarget 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):
- If you’re on ABP’s MVC/Razor Pages UI you can use ABP’s DOM API
// 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 withoutform-select, LeptonX won’t wrap them. - Or replace the control with a Blazor component/select implementation that doesn’t rely on LeptonX’s overlay.
- Don’t use the CSS class that LeptonX targets (you mentioned
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:- remove existing
.custom-select-wrappercontaining the select - re-run
processSelect(select)after the render cycle
That avoids duplication and stale markup.
- remove existing
Product fix: there is no public/verified documentation for a
dispose/destroyAPI onleptonx.FormSelectin 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)