Content
View differences
Updated by Alexander Coles 12 days ago
## Context
On every Turbo navigation, four independent handlers react to the page changing — bound to different Turbo events, in no guaranteed order, with no single module owning "what happens when we navigate": the Angular re-bootstrap (`turbo-angular-wrapper.ts`), per-navigation service re-registration (`app.module.ts` `initializeServices`), the ui-router sync (`openproject.routes.ts`), and legacy DOM setup (`turbo-global-listeners.ts`). Turbo's `turbo:load`/`turbo:render` events are gappy and overlapping — neither fires in every navigation scenario (full load, first visit, subsequent visit, 422 form error) — so each handler independently guesses which events to bind and relies on idempotency to cover the gaps (see #65298). The "is this an Angular page?" decision is separately re-derived three times via `querySelector('openproject-base')`.
<mention class="mention" data-id="76518" data-type="work_package" data-text="#OP-19617" data-display-id="OP-19617">#OP-19617</mention> OP-19617 landed the Turbo jsdom test coverage4 improvements hat seam that makes this tractable; #OP-19618 (CSP nonce) and <mention class="mention" data-id="76644" data-type="work_package" data-text="#OP-19666" data-display-id="OP-19666">#OP-19666</mention> #OP-19666 (morph policy) split off the cross-cutting cleanups.
## Goal
Give the Angular↔Turbo navigation lifecycle one owner, so navigation behaviour is legible in one place, testable through a single interface, and cleanly editable as the ui-router is removed (#DREAM-45).
## Approach
Two scoped candidates first (A + B):
* A named `navigation-lifecycle` module that owns the Turbo event truth-table behind two semantic moments: `onBodyRendered` (fires on render or load, deduped once per navigation, all pages — legacy DOM setup + service re-registration) and `onAngularVisit` (fires on a completed Drive visit, Angular-hosted pages only — root teardown + re-bootstrap + router sync). Existing handlers become injected collaborators; only the module binds Turbo events.
* A single `isAngularHostedPage()` predicate owning the `openproject-base` sentinel, replacing the three ad-hoc queries.
Behaviour is preserved except two guarded improvements: first-visit body-setup dedupes 2×→1×, and per-navigation service re-registration routes through the module. The router-sync step is isolated as a named seam so #DREAM-45 can delete it in one place.
## Notes
Fixing the double-selected-menu bug (#DREAM-406) and retiring per-navigation re-bootstrap entirely are sequenced as follow-ups on top of this module, the latter gated on #DREAM-45.
Supersedes the exploratory spike in https://github.com/opf/openproject/pull/20569.
On every Turbo navigation, four independent handlers react to the page changing — bound to different Turbo events, in no guaranteed order, with no single module owning "what happens when we navigate": the Angular re-bootstrap (`turbo-angular-wrapper.ts`), per-navigation service re-registration (`app.module.ts` `initializeServices`), the ui-router sync (`openproject.routes.ts`), and legacy DOM setup (`turbo-global-listeners.ts`). Turbo's `turbo:load`/`turbo:render` events are gappy and overlapping — neither fires in every navigation scenario (full load, first visit, subsequent visit, 422 form error) — so each handler independently guesses which events to bind and relies on idempotency to cover the gaps (see #65298). The "is this an Angular page?" decision is separately re-derived three times via `querySelector('openproject-base')`.
<mention class="mention" data-id="76518" data-type="work_package" data-text="#OP-19617" data-display-id="OP-19617">#OP-19617</mention>
## Goal
Give the Angular↔Turbo navigation lifecycle one owner, so navigation behaviour is legible in one place, testable through a single interface, and cleanly editable as the ui-router is removed (#DREAM-45).
## Approach
Two scoped candidates first (A + B):
* A named `navigation-lifecycle` module that owns the Turbo event truth-table behind two semantic moments: `onBodyRendered` (fires on render or load, deduped once per navigation, all pages — legacy DOM setup + service re-registration) and `onAngularVisit` (fires on a completed Drive visit, Angular-hosted pages only — root teardown + re-bootstrap + router sync). Existing handlers become injected collaborators; only the module binds Turbo events.
* A single `isAngularHostedPage()` predicate owning the `openproject-base` sentinel, replacing the three ad-hoc queries.
Behaviour is preserved except two guarded improvements: first-visit body-setup dedupes 2×→1×, and per-navigation service re-registration routes through the module. The router-sync step is isolated as a named seam so #DREAM-45 can delete it in one place.
## Notes
Fixing the double-selected-menu bug (#DREAM-406) and retiring per-navigation re-bootstrap entirely are sequenced as follow-ups on top of this module, the latter gated on #DREAM-45.
Supersedes the exploratory spike in https://github.com/opf/openproject/pull/20569.