Description
Step 1 of 4 of the Hotwire Turbo plan worked out in recomaty/recomaty-panel#651 (research).
The plan: 1. #21 turbo: sidebar · 2. #22 422 for invalid forms · 3. #23 turbo: full · 4. #24 adopting turbo: full in an application.
Goal
A click on a sidebar link loads the next page without reloading the document. Turbo Drive fetches the page and swaps <body>. The sidebar is rendered again, identical, and put back in its place, so only the content visibly changes. Every other link and every form still does a full page load. The feature is opt-in: nothing changes for applications that don't switch it on.
Why Drive scoped to the sidebar, not a Turbo Frame around the content
"Load only the right part" sounds like a frame, but a frame leaves the rest of the page stale:
- A frame doesn't touch
<head>, so the tab title stays the old page's. - The sidebar is outside the frame. Its active link and the group open around it stay the old page's, and both would need a sync script.
- Controllers on
<body>(adminata-layout,adminata-sticky, standard_layout.html.twig#L116-L127) keep the old page's elements.adminata-stickystarts watching the action bar only inconnect()(sticky_controller.js#L21). - Links and forms inside the content would navigate inside the frame unless each gets
target="_top".
Drive gets all of these right with no extra code. A prototype in an application built on adminata (recomaty/recomaty-panel#648) measured 85–234 ms from click to rendered page through Drive, against 377–442 ms for a full load, over a few dozen sidebar screens with no fallback and no new JS errors.
Work in adminata, with complexity
- Dependency and bundle (~0.25 day, low)
- Add
@hotwired/turbo8.0.23. - Build it as a separate
turbo.jsentry next toapp.js(vite.config.js#L62), with its own entry in .size-limit.json. - Turbo is ~94 KB minified / ~26 KB gzip.
app.jsis already 86 KB of its 100 KB limit (PLAN/05), so Turbo can't go inside it.
- Add
- Option (~0.25 day, low)
- Add
adminata.options.turbo: false | sidebar: an enum, defaultfalse;fullcomes in step 3. It goes into Configuration.php#L312. - With
sidebar, the layout loadsturbo.js(deferred, with theadminata_script_attributesblock, #L76) and setsTurbo.session.drive = false. - It also puts
data-turbo="true"on.adm-sidebar-nav, so only the menu's links go through Drive.
- Add
- Sidebar place script
adminata_sidebar_place(#L321-L384) (~0.25 day, low–medium)- Today it saves the offset on
pagehide(#L375) and places the menu again onDOMContentLoaded(#L374). A Turbo visit fires neither event. - Add: save the offset on
turbo:before-render, and place the menu once more onturbo:load. - Turbo runs the inline script again when it inserts the new body; the prototype relied on that. The listeners on
window/documentmust not pile up with every visit.
- Today it saves the offset on
- Back/forward cache preview (~0.25–0.5 day, medium)
- On
turbo:before-cache, close the rail popup, dropdowns and dialogs and clear the menu filter. Otherwise going back briefly shows a stale open popup. - Cheaper first version:
<meta name="turbo-cache-control" content="no-preview">.
- On
- Asset tracking (~0.1 day, low)
- CSP (~0.1 day, low)
- Turbo's progress bar injects a
<style>. Under a strict CSP that needs<meta name="csp-nonce">, filled from the same nonce asadminata_script_attributes.
- Turbo's progress bar injects a
- Tests and docs (~0.5 day, medium)
- Panther (real Firefox,
tests-adminata/Functional/BasePantherTestCase.php):- a sidebar click swaps the page without reloading the document (a marker set on
windowsurvives); - the title, URL, active link, open group and back/forward are right;
- a link outside the sidebar still does a full load.
- a sidebar click swaps the page without reloading the document (a marker set on
- Playwright: the menu keeps its place on the first frame after a Turbo visit, as it does after a full load (Sidebar: the menu keeps its place across page loads, from the first paint #19, Sidebar: the released version below the menu's last item, never over it #20).
- vitest: the place script's new listeners (
assets/js/__tests__/sidebar_place.test.js). - A cookbook page and a CHANGELOG entry.
- Panther (real Firefox,
adminata total: ~1.5–2 days, one PR, low risk. It is opt-in: with turbo: false the rendered HTML stays as it is.
Then in an application (~0.5 day for a typical one)
- Bump adminata and set
turbo: sidebarin the application'sadminataconfiguration. - The application's own scripts that run only on
DOMContentLoadedstop working after the first Turbo visit, and so do one-time start-up calls of libraries. Each one becomes a small Stimulus controller, or listens toturbo:loadas well (~1 h each). In the first adopting application there are two such places. - Symfony's
csrf_protection_controller.jsalready handles Turbo submissions; nothing to do there. - A pass through every sidebar screen with the final code.
Done when
- With
turbo: sidebar, a sidebar click renders the next page without reloading the document. The title, active link, open group and back/forward are right, and the menu stays in place. - With
turbo: false, nothing changes. - Panther, Playwright and vitest cover all of the above, and the size limits pass.
Created by Bober from recomaty/recomaty-panel#651 at Bartosz's request.
🦫 ~Bober
Activity