Audience: Front‑end developers
Goal: Ensure the Pluro accessibility component (scanner/fixes/widget) re‑initializes correctly on client‑side route changes and dynamic DOM updates in Single‑Page Applications (SPAs).
Replace API placeholders with your actual calls, e.g.
window.Pluro.mount(...)
,window.Pluro.destroy()
, orwindow._pluroScan()
.
1) Why SPAs need special handling
In React/Angular apps, navigation uses the History API (no full page refresh). If your accessibility script runs only once on initial load, it won’t re‑run on route changes or after lazy‑loaded content appears. You must:
- Hook SPA routing (React Router / Angular Router events).
- Observe DOM changes (e.g., content mounting/unmounting) with
MutationObserver
. - Re‑init (scan/mount/fixes) after each route or major DOM change.
- Avoid duplicates/leaks with
destroy()
before re‑init and debounce.
2) Universal concepts & patterns
2.1 Re‑init function (idempotent)
Create a single function that can be safely called many times:
async function pluroReinit() {
try {
// 1) Clean up previous instance if applicable
if (window.__pluroInstance?.destroy) {
window.__pluroInstance.destroy();
}
// 2) Give the framework a moment to paint the new view
await new Promise(r => setTimeout(r, 150));
// 3) Run your component/scan
if (window.Pluro?.mount) {
window.__pluroInstance = window.Pluro.mount({ root: document });
} else if (typeof window._pluroScan === 'function') {
window._pluroScan();
}
// 4) Optional: run idempotent DOM fixes
if (typeof window.applyPluroFixes === 'function') {
await window.applyPluroFixes();
}
} catch (e) {
// swallow non‑critical errors to avoid blocking UX
}
}
const debouncedReinit = ((fn, wait=200) => {
let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), wait); };
})(pluroReinit);
2.2 Observing content root
Pick a stable content root (examples: #root
, #app-root
, <main>
, [router-outlet]
):
function attachDomObserver(rootSelector = '#app-root') {
const root = document.querySelector(rootSelector) || document.body;
if (!root) return;
if (window.__pluroObserver) try { window.__pluroObserver.disconnect(); } catch {}
window.__pluroObserver = new MutationObserver(debouncedReinit);
window.__pluroObserver.observe(root, { childList: true, subtree: true });
}
3) React integration
3.1 Using React Router v6 (useLocation
)
Hook into location changes in your top‑level layout (once):
// AppPluroBridge.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export default function AppPluroBridge() {
const location = useLocation();
useEffect(() => {
// on route change
if (window.debouncedReinit) window.debouncedReinit();
}, [location.pathname, location.search, location.hash]);
useEffect(() => {
// on initial mount
if (window.attachDomObserver) window.attachDomObserver('#root');
if (window.debouncedReinit) window.debouncedReinit();
return () => {
// cleanup on unmount (rare for app shell)
if (window.__pluroObserver) try { window.__pluroObserver.disconnect(); } catch {}
if (window.__pluroInstance?.destroy) window.__pluroInstance.destroy();
};
}, []);
return null; // bridge only
}
Mount this bridge near your Router (e.g., inside App.tsx
):
function App() {
return (
<BrowserRouter>
<AppPluroBridge />
{/* your routes */}
</BrowserRouter>
);
}
3.2 React Portals/Modals
If you render modals/menus in portals (createPortal
), they may live outside the main root. Either:
- Observe
document.body
instead of#root
, or - Observe both: call
attachDomObserver('#root')
and a second observer for the portal container (e.g.,#modal-root
).
3.3 Next.js / Remix
For frameworks with SSR + hydration, trigger debouncedReinit()
in a client‑only effect after hydration (e.g., useEffect
in a top‑level layout). The same useLocation
pattern applies if you use their router hooks.
4) Angular integration
4.1 Subscribe to Router events
Use NavigationEnd
to trigger a re‑init after each successful navigation:
// pluro-bridge.service.ts
import { Injectable, NgZone } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class PluroBridgeService {
private attached = false;
constructor(private router: Router, private zone: NgZone) {}
init() {
if (!this.attached) {
this.attached = true;
// 1) Observe SPA route changes
this.router.events.pipe(filter(e => e instanceof NavigationEnd)).subscribe(() => {
this.zone.runOutsideAngular(() => {
// give Angular change detection time to render
setTimeout(() => window.debouncedReinit && window.debouncedReinit(), 0);
});
});
// 2) Observe DOM root
setTimeout(() => {
if (window.attachDomObserver) window.attachDomObserver('app-root');
if (window.debouncedReinit) window.debouncedReinit();
});
}
}
}
Call it from AppComponent
:
// app.component.ts
constructor(private pluroBridge: PluroBridgeService) {}
ngOnInit() {
this.pluroBridge.init();
}
4.2 Angular laziness & microtasks
For heavy pages (lazy modules, data‑driven templates), use a small delay (setTimeout(..., 50–200ms)
) before re‑init so the view is painted. Keep the debounce in place to avoid multiple runs.
5) Drop‑in (no app code changes)
If you cannot touch app code, inject a generic wrapper that hooks the History API and DOM root:
(function(){
const ROOT = '#app-root'; // fallback to body if not found
// History hooks
const dispatch = () => window.debouncedReinit && window.debouncedReinit();
const _push = history.pushState; history.pushState = function(){ const r=_push.apply(this, arguments); dispatch(); return r; };
const _rep = history.replaceState; history.replaceState = function(){ const r=_rep.apply(this, arguments); dispatch(); return r; };
window.addEventListener('popstate', dispatch);
window.addEventListener('hashchange', dispatch);
// DOM observer
if (window.attachDomObserver) window.attachDomObserver(ROOT);
// initial
if (window.debouncedReinit) window.debouncedReinit();
})();
Use this via your Advanced Code injector or a site‑wide script tag.
6) Writing safe DOM fixes
Make your fixes idempotent (safe to call repeatedly) and avoid piling event handlers:
window.applyPluroFixes = async function applyPluroFixes(){
const $ = window.jQuery || window.$; if (!$) return;
// Attributes: set only if missing/different
const setAttrOnce = ($el, name, val) => $el.each(function(){ if (this.getAttribute(name) !== String(val)) this.setAttribute(name, val); });
const rmAttrOnce = ($el, name) => $el.each(function(){ if (this.hasAttribute(name)) this.removeAttribute(name); });
setAttrOnce($('#nav_logo .finq-icon svg'), 'aria-label', 'finq logo');
rmAttrOnce($('#nav_logo .finq-icon svg'), 'title');
// Events: namespace + off before on
$('[role="rowgroup"] a>div[role="cell"] .gap-xs')
.off('click.pluro')
.on('click.pluro', function(){ $(this).closest('a[role="row"]').trigger('click'); });
};
Do: off('event.namespace').on('event.namespace', handler)
Don’t: Add plain .on('click', ...)
repeatedly without cleanup.
7) Performance & reliability
- Debounce route/DOM triggers (150–300ms) to prevent bursts.
- Destroy previous instances before re‑init to prevent leaks.
- Scope observers to a root container; avoid observing the entire document unless necessary.
- Portals/overlays: also observe the portal container if used.
- jQuery presence: if your fixes rely on jQuery, ensure it’s loaded before running them (or rewrite to vanilla JS).
- SSR/CSR: only run in the browser (inside
useEffect
/ngOnInit
, not during SSR).
8) Troubleshooting
- Nothing happens on navigation: confirm your hook runs (add a temporary
console.log
), check you’re using the correct router hook (React v6useLocation
, AngularNavigationEnd
). - Runs too early: increase the
setTimeout
beforepluroReinit()
to 150–300ms. - Repeated handlers: ensure you use namespaced events +
.off()
before.on()
. - Extension console noise: messages like “message channel closed” usually come from browser extensions; test in Incognito.
- Root not found: verify the content root selector; if uncertain, observe
document.body
.
9) Minimal checklist
- Implement
pluroReinit()
and debounced trigger. - Hook router changes (React
useLocation
/ AngularNavigationEnd
). - Attach
MutationObserver
on the content root (and portal root if any). - Call destroy() before each init.
- Make DOM fixes idempotent; namespace events.
- Test: initial load, route change, modal/dialog open, dynamic list update.
10) Security & stability
- Keep all code read‑only for app state; your script should not mutate business logic.
- Avoid blocking UI threads; swallow non‑critical errors.
- Feature‑flag your fixes in production and expose a quick way to disable them per route if needed.
11) Example folder structure (optional)
/accessible
├─ pluro-bridge/
│ ├─ react/AppPluroBridge.tsx
│ └─ angular/pluro-bridge.service.ts
├─ core/
│ ├─ reinit.js
│ └─ dom-fixes.js
└─ vendor/
└─ pluro.min.js (or loaded via CDN)
This structure keeps the SPA bridge code separate from your product logic.
Need a tailored snippet? Share your app’s root selector(s) and your exact Pluro API (e.g., mount
, destroy
, scan
), and we’ll drop in a ready‑to‑paste block for your project.