Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,7 @@ import {
finalizeQueryParamChange as finalizeClassicQueryParamChange,
queryParamsDidChange as classicQueryParamsDidChange,
} from './query-params';
import {
type ActiveTransition,
enterErrorSubstate as enterClassicErrorSubstate,
enterLoadingSubstate as enterClassicLoadingSubstate,
fireLoadingEvent,
} from './substates';
import { type ActiveTransition, findSubstateName } from './substates';

type TransitionLike = Transition & {
isAborted?: boolean;
Expand Down Expand Up @@ -106,11 +101,11 @@ export class ClassicRouteManager implements RouteManagerWithClassicInterop<Class
// Schedule the classic `loading` event rather than entering the substate
// directly: the event bubbles through `actions.loading` handlers first,
// and only the router's default handler (dispatching back through
// `enterLoadingSubstate` below) enters the substate.
// `handleLoadingEvent` below) enters the substate.
bucket.loadingSubstateTimer = scheduleOnce(
'routerTransitions',
null,
fireLoadingEvent,
this,
this.triggerLoadingEvent,
bucket,
transition
);
Expand Down Expand Up @@ -207,8 +202,8 @@ export class ClassicRouteManager implements RouteManagerWithClassicInterop<Class
// No-op for classic routes.
}

getInvokable(bucket: ClassicRouteBucket): object {
return buildClassicInvokable(bucket);
getInvokable(bucket: ClassicRouteBucket): Promise<object> {
return RSVPPromise.resolve(buildClassicInvokable(bucket));
}

qp(bucket: ClassicRouteBucket): QueryParamMeta {
Expand Down Expand Up @@ -304,30 +299,56 @@ export class ClassicRouteManager implements RouteManagerWithClassicInterop<Class
bucket.route.redirect(context as never, transition);
}

enterLoadingSubstate(
triggerLoadingEvent(bucket: ClassicRouteBucket, transition: Transition): void {
const active = transition as ActiveTransition;
if (!active.isActive) {
return;
}

active.trigger?.(true, 'loading', active, bucket.route);
}

triggerErrorEvent(
_bucket: ClassicRouteBucket,
transition: Transition,
error: Error,
route: unknown
): void {
const active = transition as ActiveTransition;

active.trigger?.(false, 'error', error, active, route);
}

handleLoadingEvent(
bucket: ClassicRouteBucket,
transition: Transition,
originRoute: unknown
): void {
enterClassicLoadingSubstate(
bucket.route._router,
originRoute as Route | undefined,
transition as ActiveTransition
);
const active = transition as ActiveTransition;
if (!active.isActive) {
return;
}

const substateName = findSubstateName(originRoute as Route | undefined, active, 'loading');
if (substateName) {
bucket.route._router.intermediateTransitionTo(substateName);
}
}

enterErrorSubstate(
handleErrorEvent(
bucket: ClassicRouteBucket,
transition: Transition,
error: Error,
originRoute: unknown
): boolean {
return enterClassicErrorSubstate(
bucket.route._router,
originRoute as Route | undefined,
transition as ActiveTransition,
error
);
const active = transition as ActiveTransition;
const substateName = findSubstateName(originRoute as Route | undefined, active, 'error');
if (!substateName) {
return false;
}

bucket.route._router.intermediateTransitionTo(substateName, error);
return true;
}

getRouteInfoMetadata(bucket: ClassicRouteBucket): unknown {
Expand Down
135 changes: 27 additions & 108 deletions packages/@ember/-internals/routing/route-managers/classic/substates.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
Classic substate detection for the route manager. Walks the active
transition's route hierarchy looking for a `*_<state>` or `*.<state>` route
matching the route currently resolving (or erroring) and triggers an
intermediate transition into it.
matching the route currently resolving (or erroring) and returns its name.
Entering the substate is the manager's job.

Mirrors the original `defaultActionHandlers.loading` and
`defaultActionHandlers.error` + `forEachRouteAbove` machinery that lived
Expand All @@ -14,14 +14,24 @@ import { assert } from '@ember/debug';
import type Owner from '@ember/-internals/owner';
import { getOwner } from '@ember/-internals/owner';
import type Route from '@ember/routing/route';
import type EmberRouter from '@ember/routing/router';
import type { InternalRouteInfo } from 'router_js';
import { getRouteManagement, hasClassicInterop, STATE_SYMBOL } from 'router_js';
import type { ClassicRouteBucket } from './bucket';
import { hasClassicInterop, STATE_SYMBOL } from 'router_js';

// Substates are classic only. A classic route has a `foo.loading`
// sibling, and only it carries the owner and names the lookup needs.
function classicRouteFor(routeInfo: InternalRouteInfo<Route>): Route | undefined {
const { manager, bucket } = routeInfo;

if (manager === undefined || bucket === undefined || !hasClassicInterop(manager)) {
return undefined;
}

return manager.getRoute(bucket) as Route;
}

export type ActiveTransition = {
isActive: boolean;
pivotHandler?: unknown;
pivotBucket?: unknown;
trigger?(ignoreFailure: boolean, name: string, ...args: unknown[]): void;
[STATE_SYMBOL]?: { routeInfos: InternalRouteInfo<Route>[] };
};
Expand All @@ -39,11 +49,6 @@ function findRouteSubstateName(route: Route, state: string) {
let owner = getOwner(route);
assert('Route is unexpectedly missing an owner', owner);

let managed = getRouteManagement(route);
if (managed === undefined || !hasClassicInterop(managed.manager)) {
return '';
}

let { routeName, fullRouteName, _router: router } = route;

let substateName = `${routeName}_${state}`;
Expand All @@ -66,11 +71,6 @@ function findRouteStateName(route: Route, state: string) {
let owner = getOwner(route);
assert('Route is unexpectedly missing an owner', owner);

let managed = getRouteManagement(route);
if (managed === undefined || !hasClassicInterop(managed.manager)) {
return '';
}

let { routeName, fullRouteName, _router: router } = route;

let stateName = routeName === 'application' ? state : `${routeName}.${state}`;
Expand All @@ -97,94 +97,6 @@ function routeHasBeenDefined(owner: Owner, router: any, localName: string, fullN
return routerHasRoute && ownerHasRoute;
}

/**
Fires the classic `loading` event for a slow transition. The event bubbles
through each route's `actions.loading` handler (public API — apps intercept
it for custom loading UI, or return `true` to keep bubbling); only if it
bubbles unhandled does the router's default `loading` action handler
dispatch back through `ClassicRouteManager.enterLoadingSubstate` to enter
the substate. Scheduled by the manager's `willEnter`; no-op if the
transition is no longer active by the time the timer fires.

@private
@param {ClassicRouteBucket} bucket
@param {Transition} transition
*/
export function fireLoadingEvent(bucket: ClassicRouteBucket, transition: ActiveTransition): void {
if (!transition.isActive) {
return;
}

transition.trigger?.(true, 'loading', transition, bucket.route);
}

/**
Look up the `loading` substate (if any) for the route that is loading
slowly and trigger an intermediate transition into it. No-op if the
transition is no longer active or no matching substate exists.

Reached via `ClassicRouteManager.enterLoadingSubstate`, which the router's
default `loading` action handler dispatches to through the classic-interop
contract once the loading event has bubbled unhandled.

@private
@param {EmberRouter} router
@param {Route|undefined} originRoute the route whose model is slow;
`undefined` when that route was never created (the walk then starts at
the transition's leaf)
@param {Transition} transition
*/
export function enterLoadingSubstate(
router: EmberRouter,
originRoute: Route | undefined,
transition: ActiveTransition
): void {
if (!transition.isActive) {
return;
}

const substateName = findSubstateName(originRoute, transition, 'loading');
if (substateName) {
router.intermediateTransitionTo(substateName);
}
}

/**
Look up the `error` substate (if any) for the route that errored and
trigger an intermediate transition into it, passing the error along so the
error route's `model` hook receives it. Returns `true` if a substate was
entered (and the error should be considered handled), `false` otherwise.

Reached via `ClassicRouteManager.enterErrorSubstate`, which the router's
default `error` action handler dispatches to through the classic-interop
contract once the error has bubbled unhandled above the application route.

@private
@param {EmberRouter} router
@param {Route|undefined} originRoute the route that errored; `undefined`
when the erroring route never got created (the walk then starts at the
transition's leaf)
@param {Transition} transition
@param {Error} error the error that triggered this substate transition
*/
export function enterErrorSubstate(
router: EmberRouter,
originRoute: Route | undefined,
transition: ActiveTransition,
error: Error
): boolean {
const substateName = findSubstateName(originRoute, transition, 'error');
if (!substateName) {
return false;
}

// Mark the error handled before transitioning so it is not re-raised
// after the substate has taken over rendering it.
router._markErrorAsHandled(error);
router.intermediateTransitionTo(substateName, error);
return true;
}

/**
Walk up from the route currently being resolved (or erroring) through the
transition's route hierarchy, returning the name of the closest matching
Expand All @@ -209,13 +121,13 @@ export function enterErrorSubstate(
@param {Transition} transition the active transition
@param {String} state the substate to look for, e.g. `loading` or `error`
*/
function findSubstateName(
export function findSubstateName(
originRoute: Route | undefined,
transition: ActiveTransition,
state: 'loading' | 'error'
): string {
const routeInfos = transition[STATE_SYMBOL]?.routeInfos ?? [];
const pivotHandler = transition.pivotHandler;
const pivotBucket = transition.pivotBucket;

const originIndex =
originRoute === undefined
Expand All @@ -226,7 +138,9 @@ function findSubstateName(

for (let i = startIndex; i >= 0; i--) {
const ancestorRouteInfo = routeInfos[i];
const ancestorRoute = ancestorRouteInfo?.route;
if (ancestorRouteInfo === undefined) continue;

const ancestorRoute = classicRouteFor(ancestorRouteInfo);
if (!ancestorRoute) continue;

if (ancestorRouteInfo !== originRouteInfo) {
Expand All @@ -237,7 +151,12 @@ function findSubstateName(
const substateName = findRouteSubstateName(ancestorRoute, state);
if (substateName) return substateName;

if (state === 'loading' && pivotHandler === ancestorRoute) break;
if (
state === 'loading' &&
pivotBucket !== undefined &&
pivotBucket === ancestorRouteInfo.bucket
)
break;
}

return '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ export interface OutletParent {
};
}

const INVOKABLES = new WeakMap<object, object>();

/**
* Represents one rendered instance of a route.
* Maps to a `routeInfo`.
*/
export class OutletState implements OutletParent {
@tracked context: unknown;

@tracked invokable: object | undefined;

readonly outlets: {
main: OutletState | undefined;
} = {
Expand All @@ -36,12 +40,31 @@ export class OutletState implements OutletParent {
}

constructor(
readonly manager: { getRouteWrapper(): object; getInvokable(bucket: object): object },
readonly manager: {
getRouteWrapper(): object;
getInvokable(bucket: object): Promise<object>;
},
readonly bucket: object,
readonly routeInfo: InternalRouteInfo<BaseRoute>
) {
this.context = routeInfo.context;

this.invokable = INVOKABLES.get(bucket);
if (this.invokable === undefined) {
// Substate routes never 'enter' and don't initialize `getInvokablePromise`
const invokablePromise = routeInfo.getInvokablePromise ?? manager.getInvokable(bucket);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Need to look for a better place to call the invokables for substates. Technically however, this is only a classicInterop feature.


invokablePromise.then(
(invokable) => {
INVOKABLES.set(bucket, invokable);
this.invokable = invokable;
},
() => {
// getInvokable rejected; this level renders nothing.
}
);
}

routeInfo.enterPromise?.then(
() => {
this.context = routeInfo.context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const asReference = internalHelper(
It's role is to enforce the shape of outlet.
*/
const PROVIDER_TEMPLATE = precompileTemplate(
'<this.component @Component={{this.bucket.invokable}} @bucket={{this.bucket}} @context={{this.state.context}} @outlet={{asReference this.childOutletRef}} />',
'<this.component @Component={{this.state.invokable}} @bucket={{this.bucket}} @context={{this.state.context}} @outlet={{asReference this.childOutletRef}} />',
{
moduleName: 'packages/@ember/-internals/routing/route-managers/outlet-arg-provider.hbs',
strictMode: true,
Expand Down
3 changes: 0 additions & 3 deletions packages/@ember/routing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,6 @@ class Route<Model = unknown> extends EmberObject.extend(ActionHandler) implement

/** @internal */
_bucketCache!: BucketCache;
/** @internal */
_internalName!: string;

private _names: unknown;

_router!: EmberRouter;
Expand Down
6 changes: 1 addition & 5 deletions packages/@ember/routing/router-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -695,11 +695,7 @@ class RouterService extends Service {
assert(`The route "${pivotRouteName}" was not found`, this._router.hasRoute(pivotRouteName));
assert(`The route "${pivotRouteName}" is currently not active`, this.isActive(pivotRouteName));

let owner = getOwner(this);
assert('RouterService is unexpectedly missing an owner', owner);
let pivotRoute = owner.lookup(`route:${pivotRouteName}`) as Route;

return this._router._routerMicrolib.refresh(pivotRoute);
return this._router._routerMicrolib.refresh(pivotRouteName);
}

/**
Expand Down
Loading
Loading