From 521a743b6e6e53f9f9c5d84261c295a7e655d702 Mon Sep 17 00:00:00 2001 From: Liam Potter Date: Fri, 8 May 2026 11:16:45 +0100 Subject: [PATCH 1/4] - use new route manager apis - make base-route an actual component with a custom component manager. --- app/route-managers/pioneer-manager.ts | 135 +++++----------------- app/route-managers/route-shell.gts | 67 +++++++++++ app/routes/BaseRoute.ts | 51 ++++++++- app/routes/application.gts | 18 +++ package.json | 7 +- pnpm-lock.yaml | 155 ++------------------------ 6 files changed, 177 insertions(+), 256 deletions(-) create mode 100644 app/route-managers/route-shell.gts diff --git a/app/route-managers/pioneer-manager.ts b/app/route-managers/pioneer-manager.ts index 1886f3c..558af0c 100644 --- a/app/route-managers/pioneer-manager.ts +++ b/app/route-managers/pioneer-manager.ts @@ -1,63 +1,21 @@ -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 { 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'; 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 +36,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; @@ -91,9 +49,6 @@ export class PioneerRouteManager { } 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}"`); } @@ -102,18 +57,15 @@ export class PioneerRouteManager { { getAncestorPromise }: { getAncestorPromise: () => Promise } ): 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 ancestorPromises = getAncestorPromise(); + console.log('ancestor promises', ancestorPromises); + const context = await bucket.route.model(ancestorPromises); + // The router writes this return value onto routeInfo.context. The + // framework's @model arg is a compute ref over routeInfo.context that + // re-reads when the outlet state dirties. The wrapper derives loading + // state from routeInfo.enterPromise (the promise this very async + // function returns), so no bucket flag is needed. + return context; } didEnter(_bucket: RouteBucket): void { @@ -132,6 +84,12 @@ export class PioneerRouteManager { console.log(`PioneerRouteManager: did exit route "${_bucket.args.name}"`); } + getRouteWrapper(_bucket: RouteBucket): 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}"` @@ -140,16 +98,16 @@ export class PioneerRouteManager { 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 +115,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