Skip to content

Implement the Route Manager API (RFC #1169)#21460

Open
evoactivity wants to merge 63 commits into
emberjs:mainfrom
mainmatter:rfc-1169-route-manager
Open

Implement the Route Manager API (RFC #1169)#21460
evoactivity wants to merge 63 commits into
emberjs:mainfrom
mainmatter:rfc-1169-route-manager

Conversation

@evoactivity

@evoactivity evoactivity commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

This is a big PR for a big feature.

Introduces a Route Manager layer between the router and route base classes so the router drives routes through a well-defined manager interface instead of calling classic Route methods directly. This decouples the router from the classic Route and is the stepping stone toward alternative route base classes and a future router. The classic Route behaves exactly as before, there are no changes to app authoring.

Three layers, top to bottom:

  • @ember/routing public re-exports of the authoring API
  • @ember/-internals/routing/route-managers The classic route manager and route manager infrastructure
  • router_js Drives the routes lifecycle through the manager, and owns the route manager contract

Why does the outlet have a legacy path?

A handful of tests directly set the outlet state, one in particular is testing something for liquid-fire. If there are addons or user code that expects to be able to create their own render state, they will not include the route wrapper and invokable we expect from the route manager, the legacy path path lets them continue working.

Query Params

The current implementation of query params is driven by the router, I have gated the parts that directly reach into routes behind the classic interop capability of route managers. I have left the "machinery" in place in the router. When we come to replace the router, a decision will need to be made if we bridge the classic router manager to use whatever new QP implementation we come up with, or if we fully migrate the router.js QP implementation to be fully encapsulated by the classic route manager.


RFC #1169

Introduce a Route Manager layer between the router and route base classes so the router drives routes through a well-defined manager interface instead of calling classic Route methods directly. This decouples the router from the classic Route and is the stepping stone toward alternative route base classes and a future router.

Add the manager interface, capabilities, and registration, implement a ClassicRouteManager that encapsulates today's classic Route behaviour behind it, and make router_js dispatch lifecycle, rendering, model resolution, and the classic-interop surface through the manager.
@evoactivity
evoactivity force-pushed the rfc-1169-route-manager branch from 37bd3e6 to 7582a9f Compare June 15, 2026 08:44
johanrd added a commit to johanrd/ember.js that referenced this pull request Jun 15, 2026
…s on emberjs#21460

Reverts the outlet.ts compute-ref guard to the current ember-7.0.0 behavior,
demonstrating that the @model-during-willDestroy instability (emberjs#18987)
still reproduces on top of the Route Manager RFC implementation (emberjs#21460).

The smoke-test job's '@model stability during route transitions' tests are
expected to fail here. Not for merge.
evoactivity and others added 6 commits July 2, 2026 20:04
…d coalesce incremental outlet renders

The outlet retakes responsibility for currying @component and @bucket onto the module-stable wrapper and keys stability on the invokable, so managers need no glimmer internals. A manager can omit the wrapper entirely (one component boundary per outlet level, ~8-10µs/level benchmarked) — measured wrapper-less, route-manager machinery is at parity with pre-manager Ember. Mid-transition outlet renders are batched onto the next timer tick (12 passes down to 2 on a 9-deep transition) without changing settle/substate timing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The outlet has a single arg-less layout again: wrapped renders curry
@Component/@bucket/@context onto the module-stable wrapper, wrapper-less
renders curry @context onto the invokable itself (curried refs stay live).
The outlet curries exactly what the target can't obtain for itself — a
module-stable wrapper can't know its route; a per-bucket invokable already
does, except for the live context ref only the outlet can build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* upstream/main:
  Post-release version bump
  Add v7.2.0-beta.1 to CHANGELOG for `ember-source`
  sanitize url attributes case-insensitively
  Improve some remaining side-effects and update tests for telling us which modules have side-effects
  [BUGFIX] Include named argument hint in debugger message for class-backed components
  [BUGFIX] Improve debugger message for template-only components

# Conflicts:
#	packages/@ember/-internals/glimmer/lib/renderer.ts
* helper carry the manager's `wrapper` (when present) plus `invokable` and
* `controller`, which the helper's stability check keys on.
*/
controller?: unknown;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good if we can move this down into the details of the legacy route manager.

// wrapper-less ones) onto the render target before handing it over, and the
// root outlet's invokable takes no args at all. Keeping the layout arg-less
// also keeps stray named args out of the debug render tree.
const OUTLET_COMPONENT_TEMPLATE = precompileTemplate('<@Component />', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably should not curry everything (because it makes component stability harder).

// The route template renders with the controller as its `self` (`this`).
// This path is for legacy `setOutletState` callers that provide a raw `template`
// We know they use controllers, so we can safely reach for the controller here.
let self = createConstRef(controller, 'this');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another place where ideally we would move knowledge of controller down inside the route manager.

/**
* Appends `into` without clearing the target first. Mimics the `appendTo` behavior for classic components.
*/
appendIntoTarget?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Porting over a comment from @NullVoxPopuli

this will need a bunch more tests <3
in particular:

renderCompnoent(A, { appendTo: IntoTarget })
renderComponent(B, { appendTo: IntoTarget })

and do the reactivity of A and B stay in the same location and not re-order themselves?
(This was the issue I ran in to during implementation, in that whatever was updated last would become last in DOM-order)
(also, I think this behavior will need its own RFC, describing the stability of render-order (and update-order), and probably appendTo rather than a booling, imo -- boolean configurable options are a bit of an ick for me)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants