diff --git a/app/route-managers/pioneer-manager.ts b/app/route-managers/pioneer-manager.ts index 1886f3c..725d9e2 100644 --- a/app/route-managers/pioneer-manager.ts +++ b/app/route-managers/pioneer-manager.ts @@ -1,63 +1,22 @@ -import { makeRouteTemplate } from '@ember/-internals/glimmer'; -import type { InternalOwner } from '@ember/-internals/owner'; -import type { RouteStateBucket } from '@ember/-internals/routing'; -import templateOnly from '@ember/component/template-only'; +import type { EnterState, RouteStateBucket } from '@ember/routing'; import { assert } from '@ember/debug'; import type Owner from '@ember/owner'; import { routeCapabilities } from '@ember/routing'; -import { precompileTemplate } from '@ember/template-compilation'; -import type { CurriedComponent, Destroyable } from '@glimmer/interfaces'; -import { getComponentTemplate, setComponentTemplate } from '@glimmer/manager'; -import { getOwner } from '@glimmer/owner'; -import type { Reference } from '@glimmer/reference'; -import { createComputeRef, createConstRef } from '@glimmer/reference'; -import { createCapturedArgs, curry, EMPTY_POSITIONAL } from '@glimmer/runtime'; -import { tracked } from '@glimmer/tracking'; -import { dict } from '@glimmer/util'; +import type { Destroyable } from '@glimmer/interfaces'; +import RouteShell from 'use-route-manager/route-managers/route-shell'; import type BaseRoute from 'use-route-manager/routes/BaseRoute'; +import { getOwner } from '@ember/owner'; const routes = import.meta.glob('../routes/**/*.gts'); -// Wrapper template-only component that switches between the route's loading -// state and its main template based on @isLoading. The route's resolved invokable -// is passed in as @RouteComponent and the optional loading template as -// @LoadingState. @model is forwarded to the route component once loading is done. -const RouteShell = templateOnly(); -setComponentTemplate( - precompileTemplate( - ` - {{#if @LoadingState}} - {{#if @isLoading}} - <@LoadingState /> - {{else}} - <@RouteComponent @model={{@model}} /> - {{/if}} - {{else}} - <@RouteComponent @model={{@model}} /> - {{/if}}`, - { strictMode: true } - ), - RouteShell -); - export class RouteBucket implements RouteStateBucket { route: BaseRoute; args: { name: string }; + // Cached after the first call to getInvokable so subsequent calls return + // the same component definition. invokable: object | undefined = undefined; - // Populated by the router synchronously after calling manager.enter(), so that - // child routes can await the parent's data resolution via getAncestorPromise. - enterPromise: Promise | undefined = undefined; - - // The resolved model returned from enter(). Tracked so that the @model ref - // in the curried invokable re-renders the template when data arrives. - @tracked context: unknown = undefined; - - // True while enter() is in flight. The wrapper template reads this to - // switch between the loading state and the resolved route component. - @tracked isLoading = true; - constructor(route: BaseRoute, args: { name: string }) { this.route = route; this.args = args; @@ -78,7 +37,7 @@ export class PioneerRouteManager { args: { name: string } ): RouteBucket { // Instantiate the plain class route using `new`, passing the owner. - // Key difference from ClassicRouteManager — no EmberObject.create(). + // Key difference from ClassicRouteManager, no EmberObject.create(). const route = new RouteClass(this.#owner); const bucket = new RouteBucket(route, args); route.bucket = bucket; @@ -86,34 +45,44 @@ export class PioneerRouteManager { return bucket; } + getRenderState(bucket: RouteBucket): object | undefined { + const route = bucket.route; + const wrapper = this.getRouteWrapper(); + const invokable = bucket.invokable; + + const owner = getOwner(route); + assert('Route is unexpectedly missing an owner', owner); + + return { + owner, + name: bucket.args.name, + controller: undefined, + model: undefined, + wrapper, + invokable, + bucket, + }; + } + getDestroyable(bucket: RouteBucket): Destroyable | null { return bucket.route; } willEnter(bucket: RouteBucket): void { - // Mark loading at the start of every enter so re-entries (same route, new - // params) flip the wrapper back to the loading state. - bucket.isLoading = true; console.log(`PioneerRouteManager: will enter route "${bucket.args.name}"`); } - async enter( - bucket: RouteBucket, - { getAncestorPromise }: { getAncestorPromise: () => Promise } - ): Promise { + async enter(bucket: RouteBucket, state: EnterState): Promise { console.log(`PioneerRouteManager: entering route "${bucket.args.name}"`); - try { - const ancestorPromises = getAncestorPromise(); - console.log('ancestor promises', ancestorPromises); - const context = await bucket.route.model(ancestorPromises); - bucket.context = context; - return context; - } finally { - // Tracked field, so the wrapper template re-renders to show the route - // component once data has arrived (or after a failure, to avoid getting - // stuck on the loading state). - bucket.isLoading = false; - } + const { to } = state; + + const ancestorContext = to.parent + ? state.getAncestorContext(to.parent) + : Promise.resolve(undefined); + + const context = await bucket.route.model(ancestorContext, to.params ?? {}); + + return context; } didEnter(_bucket: RouteBucket): void { @@ -132,24 +101,31 @@ export class PioneerRouteManager { console.log(`PioneerRouteManager: did exit route "${_bucket.args.name}"`); } + getRouteWrapper(): object { + // Module stable wrapper, the same definition is returned for every + // bucket. Per route data flows in via @routeInfo at render time. + return RouteShell; + } + async getInvokable(bucket: RouteBucket): Promise { console.log( `PioneerRouteManager: getInvokable for route "${bucket.args.name}"` ); + if (bucket.invokable !== undefined) { return bucket.invokable; } - const owner = getOwner(bucket.route)! as InternalOwner; - - // Pull the named LoadingState export off the route module if it has one. - // Routes that omit it will render the route template immediately. + // Pull the named LoadingState export off the route module if it has one + // and stash it on the route instance so the wrapper can read it via + // @routeInfo.route.LoadingState. Routes that omit the export leave the + // field undefined and the wrapper renders the route component immediately. const routePath = `../routes/${bucket.args.name.replace(/\./g, '/')}.gts`; const routeModule = (await routes[routePath]?.()) as | { LoadingState?: object; default: object } | undefined; - const LoadingState = routeModule?.LoadingState; const RouteClass = routeModule?.default; + bucket.route.LoadingState = routeModule?.LoadingState; assert( `PioneerRouteManager: failed to load route class for "${bucket.args.name}". ` + @@ -157,46 +133,7 @@ export class PioneerRouteManager { RouteClass ); - // Retrieve the template factory from the co-located .gts class and wrap it - // in a RouteTemplate so it can be rendered as a component. - const templateFactory = getComponentTemplate(RouteClass); - if (!templateFactory) { - throw new Error( - `PioneerRouteManager: no template found for route "${bucket.args.name}". ` + - `Make sure the route class is defined in a .gts file with a co-located