Skip to content

Commit 00b42fd

Browse files
committed
Handle setting up controller and model in the classic route manager.
This make outlets and renderer unaware of controllers and models. The Route Manager now curries the invokable with @controller and @model arguments. Model is @Tracked so it rerenders when it changes.
1 parent 8689563 commit 00b42fd

12 files changed

Lines changed: 93 additions & 106 deletions

File tree

packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export interface OutletDefinitionState {
4242
ref: Reference<OutletState | undefined>;
4343
name: string;
4444
template: object;
45-
controller: unknown;
4645
}
4746

4847
const CAPABILITIES: InternalComponentCapabilities = {
@@ -172,10 +171,7 @@ class OutletComponentManager
172171

173172
const OUTLET_MANAGER = new OutletComponentManager();
174173

175-
const OUTLET_COMPONENT_TEMPLATE = precompileTemplate(
176-
'<@Component @controller={{@controller}} @model={{@model}} />',
177-
{ strictMode: true }
178-
);
174+
const OUTLET_COMPONENT_TEMPLATE = precompileTemplate('<@Component />', { strictMode: true });
179175

180176
export class OutletComponent implements ComponentDefinition<
181177
OutletDefinitionState,

packages/@ember/-internals/glimmer/lib/renderer.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,7 @@ export class Renderer extends BaseRenderer {
861861
// we can refactor this to do something more direct/less convoluted
862862
// and with less setup, but get it working first
863863
let outlet = createRootOutlet(view);
864-
let { name, /* controller, */ template } = view.state;
864+
let { name, template } = view.state;
865865

866866
let named = dict<Reference>();
867867

@@ -870,18 +870,6 @@ export class Renderer extends BaseRenderer {
870870
'@Component'
871871
);
872872

873-
// TODO: is this guaranteed to be undefined? It seems to be the
874-
// case in the `OutletView` class. Investigate how much that class
875-
// exists as an internal implementation detail only, or if it was
876-
// used outside of core. As far as I can tell, test-helpers uses
877-
// it but only for `setOutletState`.
878-
// named['controller'] = createConstRef(controller, '@controller');
879-
// Update: at least according to the debug render tree tests, we
880-
// appear to always expect this to be undefined. Not a definitive
881-
// source by any means, but is useful evidence
882-
named['controller'] = UNDEFINED_REFERENCE;
883-
named['model'] = UNDEFINED_REFERENCE;
884-
885873
let args = createCapturedArgs(named, EMPTY_POSITIONAL);
886874

887875
this._appendDefinition(

packages/@ember/-internals/glimmer/lib/syntax/outlet.ts

Lines changed: 8 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,9 @@ import type {
88
Template,
99
} from '@glimmer/interfaces';
1010
import type { Reference } from '@glimmer/reference';
11-
import {
12-
childRefFromParts,
13-
createComputeRef,
14-
createConstRef,
15-
createDebugAliasRef,
16-
valueForRef,
17-
} from '@glimmer/reference';
11+
import { createComputeRef, createConstRef, valueForRef } from '@glimmer/reference';
1812
import type { CurriedValue } from '@glimmer/runtime';
19-
import { createCapturedArgs, curry, EMPTY_POSITIONAL } from '@glimmer/runtime';
13+
import { createCapturedArgs, curry, EMPTY_POSITIONAL, isCurriedValue } from '@glimmer/runtime';
2014
import { dict } from '@glimmer/util';
2115
import { hasInternalComponentManager } from '@glimmer/manager';
2216
import { OutletComponent, type OutletDefinitionState } from '../component-managers/outlet';
@@ -88,16 +82,14 @@ export const outletHelper = internalHelper(
8882

8983
let named = dict<Reference>();
9084

91-
// Here we either have a raw template that needs to be normalized,
92-
// or a component that we can render as-is. `RouteTemplate` upgrades
93-
// the template into a component so we can have a unified code path.
94-
// We still store the original `template` value, because we rely on
95-
// its identity for the stability check, and the `RouteTemplate`
96-
// wrapper doesn't dedup for us.
85+
// When the route manager provides the invokable, state.template is
86+
// already a curried component with @controller and @model baked in.
87+
// For backward compatibility (e.g. setOutletState from test helpers),
88+
// raw templates and components are wrapped with makeRouteTemplate.
9789
let template = state.template;
9890
let component: object;
9991

100-
if (hasInternalComponentManager(template)) {
92+
if (isCurriedValue(template) || hasInternalComponentManager(template)) {
10193
component = template;
10294
} else {
10395
if (DEBUG) {
@@ -137,36 +129,8 @@ export const outletHelper = internalHelper(
137129
component = makeRouteTemplate(outletOwner, state.name, template as Template);
138130
}
139131

140-
// Component is stable for the lifetime of the outlet
141132
named['Component'] = createConstRef(component, '@Component');
142133

143-
// Controller is stable for the lifetime of the outlet
144-
named['controller'] = createConstRef(state.controller, '@controller');
145-
146-
// Create a ref for the model
147-
let modelRef = childRefFromParts(outletRef, ['render', 'model']);
148-
149-
// Store the value of the model
150-
let model = valueForRef(modelRef);
151-
152-
// Create a compute ref which we pass in as the `{{@model}}` reference
153-
// for the outlet. This ref will update and return the value of the
154-
// model _until_ the outlet itself changes. Once the outlet changes,
155-
// dynamic scope also changes, and so the original model ref would not
156-
// provide the correct updated value. So we stop updating and return
157-
// the _last_ model value for that outlet.
158-
named['model'] = createComputeRef(() => {
159-
if (lastState === state) {
160-
model = valueForRef(modelRef);
161-
}
162-
163-
return model;
164-
});
165-
166-
if (DEBUG) {
167-
named['model'] = createDebugAliasRef!('@model', named['model']);
168-
}
169-
170134
let args = createCapturedArgs(named, EMPTY_POSITIONAL);
171135

172136
// Package up everything
@@ -204,7 +168,6 @@ function stateFor(
204168
ref,
205169
name: render.name,
206170
template,
207-
controller: render.controller,
208171
};
209172
}
210173

@@ -216,5 +179,5 @@ function isStable(
216179
return false;
217180
}
218181

219-
return state.template === lastState.template && state.controller === lastState.controller;
182+
return state.template === lastState.template;
220183
}

packages/@ember/-internals/glimmer/lib/utils/outlet.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,9 @@ export interface RenderState {
1515
name: string;
1616

1717
/**
18-
* The controller (the self of the outlet component)
19-
*/
20-
controller: unknown;
21-
22-
/**
23-
* The model (the resolved value of the model hook)
24-
*/
25-
model: unknown;
26-
27-
/**
28-
* The route's template – this is either a Template or a component, and it
29-
* gets normalized during the render process.
18+
* The route's invokable – a component with @controller and @model
19+
* already applied by the route manager. This is either a CurriedValue
20+
* (for classic routes) or a raw component/template for backward compatibility.
3021
*/
3122
template: Template | object | undefined;
3223
}

packages/@ember/-internals/glimmer/lib/views/outlet.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ export default class OutletView {
6868
render: {
6969
owner: owner,
7070
name: TOP_LEVEL_NAME,
71-
controller: undefined,
72-
model: undefined,
7371
template,
7472
},
7573
};
@@ -89,7 +87,6 @@ export default class OutletView {
8987
ref,
9088
name: TOP_LEVEL_NAME,
9189
template,
92-
controller: undefined,
9390
};
9491
}
9592

packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1686,7 +1686,7 @@ if (ENV._DEBUG_RENDER_TREE) {
16861686
this.outlet({
16871687
type: 'route-template',
16881688
name: '-top-level',
1689-
args: { positional: [], named: { controller: undefined, model: undefined } },
1689+
args: { positional: [], named: {} },
16901690
instance: undefined,
16911691
template: outlet,
16921692
bounds: this.elementBounds(this.element!),

packages/@ember/-internals/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212
"./error-handling": "./error-handling/index.ts",
1313
"./glimmer": "./glimmer/index.ts",
1414
"./glimmer/lib/components/internal": "./glimmer/lib/components/internal.ts",
15+
"./glimmer/lib/component-managers/route-template": "./glimmer/lib/component-managers/route-template.ts",
1516
"./meta": "./meta/index.ts",
1617
"./metal": "./metal/index.ts",
1718
"./owner": "./owner/index.ts",
1819
"./routing": "./routing/index.ts",
20+
"./routing/route-managers/utils": "./routing/route-managers/utils.ts",
21+
"./routing/route-managers/classic-route-manager": "./routing/route-managers/classic-route-manager.ts",
22+
"./routing/route-managers/route-manager": "./routing/route-managers/route-manager.ts",
1923
"./runtime": "./runtime/index.ts",
2024
"./string": "./string/index.ts",
2125
"./utility-types": "./utility-types/index.ts",

packages/@ember/-internals/routing/route-managers/classic-route-manager.ts

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { assert, info } from '@ember/debug';
33
import { get } from '@ember/-internals/metal';
44
import { DEBUG } from '@glimmer/env';
55
import { hasInternalComponentManager } from '@glimmer/manager';
6-
import type { Destroyable, TemplateFactory } from '@glimmer/interfaces';
6+
import type { CurriedComponent, Destroyable, Template, TemplateFactory } from '@glimmer/interfaces';
7+
import type { Reference } from '@glimmer/reference';
8+
import { createComputeRef } from '@glimmer/reference';
9+
import { createCapturedArgs, curry, EMPTY_POSITIONAL } from '@glimmer/runtime';
10+
import { dict } from '@glimmer/util';
11+
import { tracked } from '@glimmer/tracking';
12+
import { makeRouteTemplate } from '@ember/-internals/glimmer/lib/component-managers/route-template';
713
import type Route from '@ember/routing/route';
814
import type {
915
RouteManager,
@@ -21,10 +27,22 @@ import { Promise } from 'rsvp';
2127

2228
// --- Bucket ---
2329

24-
export interface ClassicRouteBucket extends RouteStateBucket {
30+
export class ClassicRouteBucket implements RouteStateBucket {
2531
route: Route;
26-
context: unknown;
32+
@tracked context: unknown;
33+
controller: unknown;
2734
invokable: object | undefined;
35+
instance: object;
36+
args: CreateRouteArgs;
37+
38+
constructor(route: Route, args: CreateRouteArgs) {
39+
this.route = route;
40+
this.context = undefined;
41+
this.controller = undefined;
42+
this.invokable = undefined;
43+
this.instance = route;
44+
this.args = args;
45+
}
2846
}
2947

3048
// --- Classic interop args ---
@@ -55,13 +73,7 @@ export class ClassicRouteManager implements RouteManager<ClassicRouteBucket> {
5573
let route = routeInstance as Route;
5674
route._setRouteName(args.name);
5775

58-
return {
59-
route,
60-
context: undefined,
61-
invokable: undefined,
62-
instance: route,
63-
args,
64-
};
76+
return new ClassicRouteBucket(route, args);
6577
}
6678

6779
getDestroyable(bucket: ClassicRouteBucket): Destroyable | null {
@@ -150,7 +162,7 @@ export class ClassicRouteManager implements RouteManager<ClassicRouteBucket> {
150162
})
151163
.then((resolvedModel) => this._runAfterModel(route, resolvedModel, transition))
152164
.then((resolvedModel) => {
153-
(bucket as any).context = resolvedModel;
165+
bucket.context = resolvedModel;
154166

155167
return resolvedModel;
156168
})
@@ -180,6 +192,10 @@ export class ClassicRouteManager implements RouteManager<ClassicRouteBucket> {
180192
route.setup(context, transition!);
181193
}
182194

195+
// Sync the controller onto the bucket so that the
196+
// the invokable (see getInvokable) picks it up.
197+
bucket.controller = route.controller;
198+
183199
if (route._environment?.options?.shouldRender !== false) {
184200
once(route._router, '_setOutlets');
185201
}
@@ -212,6 +228,11 @@ export class ClassicRouteManager implements RouteManager<ClassicRouteBucket> {
212228
// --- Template/Component lookup ---
213229

214230
getInvokable(bucket: ClassicRouteBucket): Promise<object | undefined> {
231+
// Return cached invokable if already built
232+
if (bucket.invokable !== undefined) {
233+
return Promise.resolve(bucket.invokable);
234+
}
235+
215236
let route = bucket.route;
216237
let owner = getOwner(route);
217238
assert('Route is unexpectedly missing an owner', owner);
@@ -222,12 +243,17 @@ export class ClassicRouteManager implements RouteManager<ClassicRouteBucket> {
222243
| object
223244
| undefined;
224245

225-
let template: object;
246+
let component: object;
247+
// Track whether the component is already a resolved definition (CurriedValue
248+
// from makeRouteTemplate) vs a raw component class that needs VM resolution.
249+
let isResolved = true;
226250

227251
if (templateFactoryOrComponent) {
228252
if (hasInternalComponentManager(templateFactoryOrComponent)) {
229-
// ComponentLike - pass through directly
230-
template = templateFactoryOrComponent;
253+
// ComponentLike - use directly, will be curried below.
254+
// Not resolved yet — the VM needs to look up its definition.
255+
component = templateFactoryOrComponent;
256+
isResolved = false;
231257
} else {
232258
if (DEBUG && typeof templateFactoryOrComponent !== 'function') {
233259
let label: string;
@@ -246,8 +272,9 @@ export class ClassicRouteManager implements RouteManager<ClassicRouteBucket> {
246272
);
247273
}
248274

249-
// TemplateFactory -> Template
250-
template = (templateFactoryOrComponent as TemplateFactory)(owner);
275+
// TemplateFactory -> Template -> RouteTemplate (curried component with no args)
276+
let template = (templateFactoryOrComponent as TemplateFactory)(owner);
277+
component = makeRouteTemplate(owner, name, template as Template);
251278
}
252279
} else {
253280
if (DEBUG) {
@@ -258,11 +285,33 @@ export class ClassicRouteManager implements RouteManager<ClassicRouteBucket> {
258285
});
259286
}
260287
}
261-
// Default {{outlet}} template
262-
template = route._topLevelViewTemplate(owner);
288+
// Default {{outlet}} template -> RouteTemplate
289+
let template = route._topLevelViewTemplate(owner);
290+
component = makeRouteTemplate(owner, name, template as Template);
263291
}
264292

265-
bucket.invokable = template;
266-
return Promise.resolve(template);
293+
// Create refs that read from the bucket. The bucket fields are populated
294+
// later (context during enter(), controller during didEnter()), so these
295+
// refs start as undefined and resolve lazily.
296+
//
297+
// Controller: a tag-free compute ref. It's read lazily (first read happens
298+
// during rendering, after didEnter has set bucket.controller).
299+
//
300+
// Model: bucket.context is @tracked, so this compute ref auto-tracks it
301+
// and re-evaluates when the model changes (e.g. navigating to the same
302+
// route with different dynamic segments).
303+
let named = dict<Reference>();
304+
named['controller'] = createComputeRef(() => bucket.controller);
305+
named['model'] = createComputeRef(() => bucket.context);
306+
307+
let args = createCapturedArgs(named, EMPTY_POSITIONAL);
308+
309+
// Curry @controller and @model onto the component so the invokable is
310+
// self-contained. The outlet rendering pipeline no longer needs to know
311+
// about model or controller.
312+
let invokable = curry(0 as CurriedComponent, component, owner, args, isResolved);
313+
314+
bucket.invokable = invokable;
315+
return Promise.resolve(invokable);
267316
}
268317
}

packages/@ember/routing/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2118,7 +2118,7 @@ export default Route;
21182118
// --- Wire the ClassicRouteManager on the Route base class ---
21192119
// Every Route subclass inherits this via the prototype-walking lookup in getRouteManager.
21202120

2121-
import { setRouteManager } from '@ember/-internals/routing';
2122-
import { ClassicRouteManager } from '@ember/-internals/routing';
2121+
import { setRouteManager } from '@ember/-internals/routing/route-managers/utils';
2122+
import { ClassicRouteManager } from '@ember/-internals/routing/route-managers/classic-route-manager';
21232123

21242124
setRouteManager(() => new ClassicRouteManager(), Route);

packages/@ember/routing/router.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import {
3434
getFullQueryParams,
3535
hasDefaultSerialize,
3636
} from '@ember/routing/route';
37-
import { getRouteManager } from '@ember/-internals/routing';
37+
import { getRouteManager } from '@ember/-internals/routing/route-managers/utils';
3838
import type {
3939
InternalRouteInfo,
4040
ModelFor,
@@ -629,8 +629,6 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented {
629629
let render = {
630630
owner: routeOwner,
631631
name: route.routeName,
632-
controller: route.controller,
633-
model: route.currentModel,
634632
template,
635633
};
636634

0 commit comments

Comments
 (0)