![Image 1 — I built a SPA router for SonataAdmin (Symfony) that requires zero backend changes — @wlindabla/sonata_spa [OSS]](https://preview.redd.it/520ja9xdow1h1.png?width=1424&format=png&auto=webp&s=9ec2677d497536b1f564bbb41b069a519440af93)
![Image 2 — I built a SPA router for SonataAdmin (Symfony) that requires zero backend changes — @wlindabla/sonata_spa [OSS]](https://preview.redd.it/p6uiwgutow1h1.png?width=1424&format=png&auto=webp&s=dd750138196608b1b9f31439633e39035daeab38)
![Image 3 — I built a SPA router for SonataAdmin (Symfony) that requires zero backend changes — @wlindabla/sonata_spa [OSS]](https://preview.redd.it/s0j6whqyow1h1.png?width=1376&format=png&auto=webp&s=0d927a325cfaab050ab7cbe1f694a792f4953373)
![Image 4 — I built a SPA router for SonataAdmin (Symfony) that requires zero backend changes — @wlindabla/sonata_spa [OSS]](https://preview.redd.it/0c56j8u4pw1h1.png?width=1424&format=png&auto=webp&s=b99bcc19e3943d19a60e06cf600624ce8bb43a14)
![Image 5 — I built a SPA router for SonataAdmin (Symfony) that requires zero backend changes — @wlindabla/sonata_spa [OSS]](https://preview.redd.it/8eougm2epw1h1.png?width=1672&format=png&auto=webp&s=9ddaa7a5e85e803ad7236d45b59538da2bc75aa0)
I built a SPA router for SonataAdmin (Symfony) that requires zero backend changes — @wlindabla/sonata_spa [OSS]
Reddit — r/symfony + r/typescript + r/webdev
Hey r/symfony,
Long-time SonataAdmin user here. I finally got fed up with full page reloads on every navigation and built a proper fix.
TL;DR: @wlindabla/sonata_spa is a TypeScript library that turns your existing SonataAdmin backend into an SPA — sidebar clicks, pagination, filters, sorting, show/delete — all handled without full reloads. Zero changes to your Symfony controllers or Twig templates.
yarn add @wlindabla/sonata_spa
> Requires: AdminLTE >= 3.x / 4.x · Bootstrap >= 5.3 · SonataAdmin >= 4.x · Node.js >= 18
Why I built this
SonataAdmin is genuinely excellent. But by default, every navigation is a full page reload — every script re-executes, every Bootstrap 5 component and every Stimulus controller re-initializes from scratch. For an internal admin panel used daily by your team, this friction is real.
Most "fixes" either:
- Replace Sonata entirely with a React frontend (lose everything Sonata gives you)
- Use Turbo (conflicts with Sonata's Stimulus outlet system —
uniqid()IDs change on every request) - Patch individual pages with custom AJAX (unmaintainable mess)
None of these are satisfying. I needed something that preserves 100% of Sonata's features while eliminating full reloads.
The architecture — Symfony concepts in TypeScript
The library is directly inspired by Symfony's HttpKernel and EventDispatcher:
User clicks link
→ BindingManager builds SpaRequest
→ SpaKernel.handle(request)
1. dispatch spa:request ← STOPPABLE
2. RequestMatcher — server-managed? → full reload if yes
3. RouteResolver.resolve(url) → RouteMatch
4. dispatch spa:route:resolved ← STOPPABLE
5. dispatch crud:list / crud:show / crud:delete / ...
→ Subscriber: fetch → DOM swap → history → Stimulus reconnect → dom:ready
Minimal setup
// assets/spa.ts
import { SpaKernel, SpaEvents } from '@wlindabla/sonata_spa';
import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher/browser';
document.addEventListener('DOMContentLoaded', () => {
const spa = SpaKernel.create(
{
router: {
sidebarSelector: '#app-sidebar',
mainSelector: '#app-main',
mainContentAreaSelector: '#app-content',
mainContentHeaderSelector: '#app-content-header',
},
},
'prod',
new BrowserEventDispatcher(window, { bubbles: true })
);
// Re-initialize your libraries after each swap
spa.getDispatcher().addListener(SpaEvents.DOM_READY, (event) => {
initMyLibraries(event.container);
});
spa.boot();
});
{# In your standard_layout.html.twig #}
{% block javascripts %}
{{ parent() }}
{{ encore_entry_script_tags('spa') }}
{% endblock %}
That's the full setup for basic usage.
Key features
- Surgical DOM swaps — only the changed parts of the page are replaced. List pages swap only the data table + filters. Show/dashboard pages swap the full
#app-main. - Correct Stimulus reconnection —
DomManagerdetects new Stimulus outlet IDs after each swap and forces reconnection viarequestAnimationFrame. - CSRF-aware delete flow — fetches the Sonata delete confirmation page, extracts the CSRF token, then POSTs with
X-Requested-With: XMLHttpRequest. SweetAlert2 modal instead of browserconfirm(). - Two-step batch flow — POSTs the batch form, parses the Sonata confirmation HTML, shows a SweetAlert2 modal, then re-POSTs with
confirmation=ok. - History API —
pushStatewithRouteMatchstored in the state object. Popstate (back/forward) replays navigation without re-resolving the URL. - Extension system — modeled after Sonata's
AdminExtensionInterface. Add custom route patterns, binding managers, subscribers, and server-managed URL rules without touching the sealed kernel. - Full TypeScript — strict types,
exactOptionalPropertyTypes: true.
Override the default confirmation UI
// Replace SweetAlert2 with your own modal — priority > 0 overrides the default
dispatcher.addListener(
SpaEvents.DELETE_CONFIRM_REQUESTED,
async (event) => {
const confirmed = await myModal.confirm(event.title, event.message);
if (confirmed) {
await event.accept(); // proceeds with DELETE POST
} else {
event.cancel();
}
},
10 // higher priority than the built-in default (0)
);
What stays server-managed (full reload)
By design, /create and /edit URLs always trigger a full reload. These pages require server-side CSRF token generation and Sonata's session management. Form submissions are handled via SPA (JSON response), but the initial page load is always a full request.
You can add more server-managed patterns in the options:
serverManagedUrlOptions: [
/\/export(\?.*)?$/,
/\/import(\?.*)?$/,
]
Links
- npm:
yarn add @wlindabla/sonata_spa - GitHub: https://github.com/Agbokoudjo/sonata_spa
- Author: AGBOKOUDJO Franck (internationaleswebservices@gmail.com)
- License: MIT
Happy to answer questions about the implementation — especially around the Stimulus reconnection logic and the CSRF flow, which were the trickiest parts to get right.
Built with ❤️ in Benin 🇧🇯 — INTERNATIONALES WEB APPS & SERVICES