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(), or window._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:

  1. Hook SPA routing (React Router / Angular Router events).
  2. Observe DOM changes (e.g., content mounting/unmounting) with MutationObserver.
  3. Re‑init (scan/mount/fixes) after each route or major DOM change.
  4. 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 v6 useLocation, Angular NavigationEnd).
  • Runs too early: increase the setTimeout before pluroReinit() 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 / Angular NavigationEnd).
  • 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.