From 68fc8072f62b938e7e43e4c177a9c58199bb09bf Mon Sep 17 00:00:00 2001 From: Chris Manson Date: Wed, 8 Jul 2026 14:19:41 +0100 Subject: [PATCH 01/13] small experiment with a new @outlet --- .../-internals/glimmer/lib/syntax/outlet.ts | 1 + .../routing/route-managers/classic/wrapper.ts | 19 +++++++++++++++++-- .../runtime/lib/compiled/opcodes/component.ts | 3 +++ .../ember/tests/routing/route_manager_test.js | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts b/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts index dc4d5f1c556..599393e053b 100644 --- a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts @@ -50,6 +50,7 @@ import type { OutletState } from '../utils/outlet'; */ export const outletHelper = /*@__PURE__*/ internalHelper( (_args: CapturedArguments, owner?: InternalOwner, scope?: DynamicScope) => { + console.log('FACE but from outlet') assert('Expected owner to be present, {{outlet}} requires an owner', owner); assert( 'Expected dynamic scope to be present. You may have attempted to use the {{outlet}} keyword dynamically. This keyword cannot be used dynamically.', diff --git a/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts b/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts index f816d97d627..045a97dd78c 100644 --- a/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts @@ -9,6 +9,7 @@ import type { CustomRenderNode, + DynamicScope, InternalComponentCapabilities, InternalComponentManager, Reference, @@ -18,14 +19,20 @@ import { setInternalComponentManager } from '@glimmer/manager/lib/internal/api'; import { setComponentTemplate } from '@glimmer/manager/lib/public/template'; import { NULL_REFERENCE } from '@glimmer/reference/lib/reference'; import { precompileTemplate } from '@ember/template-compilation'; +import { outletHelper } from '@ember/-internals/glimmer/lib/syntax/outlet'; // Renders the invokable passed in as `@Component` and forwards // `@model` / `@controller` onto it. const CLASSIC_WRAPPER_TEMPLATE = precompileTemplate( - `<@Component @model={{@context}} @controller={{@bucket.controller}} />`, + `<@Component @model={{@context}} @controller={{@bucket.controller}} @outlet={{outlet}}/>`, { moduleName: 'packages/@ember/-internals/routing/route-managers/classic/wrapper.hbs', strictMode: true, + scope() { + return { + outlet: outletHelper + } + }, } ); @@ -34,6 +41,14 @@ class ClassicRouteWrapperManager InternalComponentManager, WithCustomDebugRenderTree { + create(_owner: unknown, + _definition: unknown, + _args: unknown, + _env: unknown, + dynamicScope: DynamicScope) { + console.log('calling create in wrapper manager') + console.log(dynamicScope) + } getCapabilities(): InternalComponentCapabilities { // Match templateOnlyComponent's capabilities: no element, no args capture, // no instance state. @@ -45,7 +60,7 @@ class ClassicRouteWrapperManager attributeHook: false, elementHook: false, createCaller: false, - dynamicScope: false, + dynamicScope: true, updateHook: false, createInstance: false, wrapped: false, diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts index ec5b2c206d1..40e82b5a587 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts @@ -384,6 +384,7 @@ APPEND_OPCODES.add(VM_PREPARE_ARGS_OP, (vm, { op1: register }) => { }); APPEND_OPCODES.add(VM_CREATE_COMPONENT_OP, (vm, { op1: flags }) => { + console.log('create component opcode') let instance = check(vm.fetchValue($s0), CheckComponentInstance); let { definition, manager, capabilities } = instance; @@ -396,6 +397,7 @@ APPEND_OPCODES.add(VM_CREATE_COMPONENT_OP, (vm, { op1: flags }) => { let dynamicScope: Nullable = null; if (managerHasCapability(manager, capabilities, InternalComponentCapabilities.dynamicScope)) { + console.log('has the capabilities') dynamicScope = vm.dynamicScope(); } @@ -411,6 +413,7 @@ APPEND_OPCODES.add(VM_CREATE_COMPONENT_OP, (vm, { op1: flags }) => { self = vm.getSelf(); } + console.log('about to call create') let state = manager.create( vm.getOwner(), definition.state, diff --git a/packages/ember/tests/routing/route_manager_test.js b/packages/ember/tests/routing/route_manager_test.js index 50a50174ea7..24cc0d3db8a 100644 --- a/packages/ember/tests/routing/route_manager_test.js +++ b/packages/ember/tests/routing/route_manager_test.js @@ -108,7 +108,7 @@ moduleFor( this.add('route:application', class extends TestRoute {}); this.add('route:index', class extends TestRoute {}); - this.add('template:application', precompileTemplate('app:{{outlet}}')); + this.add('template:application', precompileTemplate('app:{{@outlet}}')); this.add('template:index', precompileTemplate('index')); } From c0522c5b0642d739b36f1f14a09e3d79f16fb35a Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Wed, 8 Jul 2026 19:58:30 +0200 Subject: [PATCH 02/13] fix: invoke outlet helper before passing it as named arg --- .../routing/route-managers/classic/wrapper.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts b/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts index 045a97dd78c..1e58210dbe1 100644 --- a/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts @@ -9,7 +9,6 @@ import type { CustomRenderNode, - DynamicScope, InternalComponentCapabilities, InternalComponentManager, Reference, @@ -24,14 +23,14 @@ import { outletHelper } from '@ember/-internals/glimmer/lib/syntax/outlet'; // Renders the invokable passed in as `@Component` and forwards // `@model` / `@controller` onto it. const CLASSIC_WRAPPER_TEMPLATE = precompileTemplate( - `<@Component @model={{@context}} @controller={{@bucket.controller}} @outlet={{outlet}}/>`, + `<@Component @model={{@context}} @controller={{@bucket.controller}} @outlet={{(outlet)}}/>`, { moduleName: 'packages/@ember/-internals/routing/route-managers/classic/wrapper.hbs', strictMode: true, scope() { return { - outlet: outletHelper - } + outlet: outletHelper, + }; }, } ); @@ -41,17 +40,7 @@ class ClassicRouteWrapperManager InternalComponentManager, WithCustomDebugRenderTree { - create(_owner: unknown, - _definition: unknown, - _args: unknown, - _env: unknown, - dynamicScope: DynamicScope) { - console.log('calling create in wrapper manager') - console.log(dynamicScope) - } getCapabilities(): InternalComponentCapabilities { - // Match templateOnlyComponent's capabilities: no element, no args capture, - // no instance state. return { dynamicLayout: false, dynamicTag: false, @@ -60,7 +49,7 @@ class ClassicRouteWrapperManager attributeHook: false, elementHook: false, createCaller: false, - dynamicScope: true, + dynamicScope: false, updateHook: false, createInstance: false, wrapped: false, From 4b0f767382c09e201f4d87d65d77d2077fb5d3c9 Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Thu, 9 Jul 2026 14:58:25 +0200 Subject: [PATCH 03/13] feat: transform {{outlet}} to <@outlet /> move outlet close to the route manager impl --- package.json | 1 + .../-internals/glimmer/lib/router-resolver.ts | 3 -- .../glimmer/lib/templates/outlet.ts | 2 +- .../route-managers/classic}/outlet.ts | 11 +++++--- .../routing/route-managers/classic/wrapper.ts | 2 +- .../transform-wrap-mount-and-outlet.ts | 28 ++++++++++--------- .../runtime/lib/compiled/opcodes/component.ts | 3 -- 7 files changed, 25 insertions(+), 25 deletions(-) rename packages/@ember/-internals/{glimmer/lib/syntax => routing/route-managers/classic}/outlet.ts (96%) diff --git a/package.json b/package.json index d59f01a4e97..ebd94d06926 100644 --- a/package.json +++ b/package.json @@ -192,6 +192,7 @@ "@ember/-internals/routing/route-managers/api.js": "ember-source/@ember/-internals/routing/route-managers/api.js", "@ember/-internals/routing/route-managers/classic/bucket.js": "ember-source/@ember/-internals/routing/route-managers/classic/bucket.js", "@ember/-internals/routing/route-managers/classic/manager.js": "ember-source/@ember/-internals/routing/route-managers/classic/manager.js", + "@ember/-internals/routing/route-managers/classic/outlet.js": "ember-source/@ember/-internals/routing/route-managers/classic/outlet.js", "@ember/-internals/routing/route-managers/classic/query-params.js": "ember-source/@ember/-internals/routing/route-managers/classic/query-params.js", "@ember/-internals/routing/route-managers/classic/substates.js": "ember-source/@ember/-internals/routing/route-managers/classic/substates.js", "@ember/-internals/routing/route-managers/classic/wrapper.js": "ember-source/@ember/-internals/routing/route-managers/classic/wrapper.js", diff --git a/packages/@ember/-internals/glimmer/lib/router-resolver.ts b/packages/@ember/-internals/glimmer/lib/router-resolver.ts index acc2c8230ef..e555bee3da6 100644 --- a/packages/@ember/-internals/glimmer/lib/router-resolver.ts +++ b/packages/@ember/-internals/glimmer/lib/router-resolver.ts @@ -3,11 +3,8 @@ import type { InternalOwner } from '@ember/-internals/owner'; import type { Nullable } from '@ember/-internals/utility-types'; import ResolverImpl from './resolver'; import { mountHelper } from './syntax/mount'; -import { outletHelper } from './syntax/outlet'; - const ROUTER_KEYWORD_HELPERS: Record = { '-mount': mountHelper, - '-outlet': outletHelper, }; /** diff --git a/packages/@ember/-internals/glimmer/lib/templates/outlet.ts b/packages/@ember/-internals/glimmer/lib/templates/outlet.ts index 803be28628f..6c8cf27386e 100644 --- a/packages/@ember/-internals/glimmer/lib/templates/outlet.ts +++ b/packages/@ember/-internals/glimmer/lib/templates/outlet.ts @@ -1,5 +1,5 @@ import { precompileTemplate } from '@ember/template-compilation'; -import { outletHelper } from '../syntax/outlet'; +import { outletHelper } from '../../../routing/route-managers/classic/outlet'; export default precompileTemplate(`{{component (outletHelper)}}`, { moduleName: 'packages/@ember/-internals/glimmer/lib/templates/outlet.hbs', diff --git a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts b/packages/@ember/-internals/routing/route-managers/classic/outlet.ts similarity index 96% rename from packages/@ember/-internals/glimmer/lib/syntax/outlet.ts rename to packages/@ember/-internals/routing/route-managers/classic/outlet.ts index 599393e053b..c44efdeb556 100644 --- a/packages/@ember/-internals/glimmer/lib/syntax/outlet.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/outlet.ts @@ -13,9 +13,12 @@ import type { CurriedValue } from '@glimmer/runtime/lib/curried-value'; import { createCapturedArgs, EMPTY_POSITIONAL } from '@glimmer/runtime/lib/vm/arguments'; import { curry } from '@glimmer/runtime/lib/curried-value'; import { dict } from '@glimmer/util/lib/collections'; -import { OutletComponent, type OutletDefinitionState } from '../component-managers/outlet'; -import { internalHelper } from '../helpers/internal-helper'; -import type { OutletState } from '../utils/outlet'; +import { + OutletComponent, + type OutletDefinitionState, +} from '../../../glimmer/lib/component-managers/outlet'; +import { internalHelper } from '../../../glimmer/lib/helpers/internal-helper'; +import type { OutletState } from '../../../glimmer/lib/utils/outlet'; /** The `{{outlet}}` helper lets you specify where a child route will render in @@ -43,6 +46,7 @@ import type { OutletState } from '../utils/outlet'; Note: Your content __will not render__ if there isn't an `{{outlet}}` for it. `outlet` is built-in and does not need to be imported. + Invokes the `-outlet` view. @method outlet @for Ember.Templates.helpers @@ -50,7 +54,6 @@ import type { OutletState } from '../utils/outlet'; */ export const outletHelper = /*@__PURE__*/ internalHelper( (_args: CapturedArguments, owner?: InternalOwner, scope?: DynamicScope) => { - console.log('FACE but from outlet') assert('Expected owner to be present, {{outlet}} requires an owner', owner); assert( 'Expected dynamic scope to be present. You may have attempted to use the {{outlet}} keyword dynamically. This keyword cannot be used dynamically.', diff --git a/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts b/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts index 1e58210dbe1..f72d1a36a08 100644 --- a/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts @@ -18,7 +18,7 @@ import { setInternalComponentManager } from '@glimmer/manager/lib/internal/api'; import { setComponentTemplate } from '@glimmer/manager/lib/public/template'; import { NULL_REFERENCE } from '@glimmer/reference/lib/reference'; import { precompileTemplate } from '@ember/template-compilation'; -import { outletHelper } from '@ember/-internals/glimmer/lib/syntax/outlet'; +import { outletHelper } from './outlet'; // Renders the invokable passed in as `@Component` and forwards // `@model` / `@controller` onto it. diff --git a/packages/@ember/template-compiler/lib/plugins/transform-wrap-mount-and-outlet.ts b/packages/@ember/template-compiler/lib/plugins/transform-wrap-mount-and-outlet.ts index ac9bf4bd17a..513a7a3cec4 100644 --- a/packages/@ember/template-compiler/lib/plugins/transform-wrap-mount-and-outlet.ts +++ b/packages/@ember/template-compiler/lib/plugins/transform-wrap-mount-and-outlet.ts @@ -29,11 +29,11 @@ import { isPath, trackLocals } from './utils'; with ```handlebars - {{component (-outlet)}} + <@outlet /> ``` @private - @class TransformHasBlockSyntax + @class TransformWrapMountAndOutlet */ export default function transformWrapMountAndOutlet(env: EmberASTPluginEnvironment): ASTPlugin { let { builders: b } = env.syntax; @@ -47,20 +47,22 @@ export default function transformWrapMountAndOutlet(env: EmberASTPluginEnvironme ...visitor, MustacheStatement(node: AST.MustacheStatement): AST.Node | void { - if ( - isPath(node.path) && - (node.path.original === 'mount' || node.path.original === 'outlet') && - !hasLocal(node.path.original) - ) { - let subexpression = b.sexpr( - b.path(`-${node.path.original}`), - node.params, - node.hash, - node.loc - ); + if (!isPath(node.path) || hasLocal(node.path.original)) { + return; + } + + if (node.path.original === 'mount') { + let subexpression = b.sexpr(b.path('-mount'), node.params, node.hash, node.loc); return b.mustache(b.path('component'), [subexpression], b.hash(), undefined, node.loc); } + + if (node.path.original === 'outlet') { + return b.element( + { path: b.fullPath(b.at('@outlet')), selfClosing: true }, + { loc: node.loc } + ); + } }, }, }; diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts index 40e82b5a587..ec5b2c206d1 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts @@ -384,7 +384,6 @@ APPEND_OPCODES.add(VM_PREPARE_ARGS_OP, (vm, { op1: register }) => { }); APPEND_OPCODES.add(VM_CREATE_COMPONENT_OP, (vm, { op1: flags }) => { - console.log('create component opcode') let instance = check(vm.fetchValue($s0), CheckComponentInstance); let { definition, manager, capabilities } = instance; @@ -397,7 +396,6 @@ APPEND_OPCODES.add(VM_CREATE_COMPONENT_OP, (vm, { op1: flags }) => { let dynamicScope: Nullable = null; if (managerHasCapability(manager, capabilities, InternalComponentCapabilities.dynamicScope)) { - console.log('has the capabilities') dynamicScope = vm.dynamicScope(); } @@ -413,7 +411,6 @@ APPEND_OPCODES.add(VM_CREATE_COMPONENT_OP, (vm, { op1: flags }) => { self = vm.getSelf(); } - console.log('about to call create') let state = manager.create( vm.getOwner(), definition.state, From b9663d8890e2f5f1a7f082daa52ebc8537e059a5 Mon Sep 17 00:00:00 2001 From: Chris Manson Date: Thu, 9 Jul 2026 16:59:18 +0100 Subject: [PATCH 04/13] WIP: fix debug render tree? --- .../application/debug-render-tree-test.ts | 183 ++++++++++-------- 1 file changed, 98 insertions(+), 85 deletions(-) diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts index 53650ae7f08..2f5efec772d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts @@ -94,13 +94,14 @@ if (ENV._DEBUG_RENDER_TREE) { await this.visit('/'); + debugger this.assertRenderTree([ this.outlet({ type: 'route-template', name: 'index', args: { positional: [], - named: { controller: this.controllerFor('index'), model: undefined }, + named: { controller: this.controllerFor('index'), model: undefined, outlet: null }, }, instance: this.controllerFor('index'), bounds: this.elementBounds(this.element!), @@ -108,89 +109,89 @@ if (ENV._DEBUG_RENDER_TREE) { }), ]); - await this.visit('/foo'); - - this.assertRenderTree([ - this.outlet({ - type: 'route-template', - name: 'foo', - args: { - positional: [], - named: { controller: this.controllerFor('foo'), model: undefined }, - }, - instance: this.controllerFor('foo'), - bounds: this.elementBounds(this.element!), - children: [ - this.outlet({ - type: 'route-template', - name: 'foo.index', - args: { - positional: [], - named: { controller: this.controllerFor('foo.index'), model: undefined }, - }, - instance: this.controllerFor('foo.index'), - bounds: this.nodeBounds(this.element!.lastChild), - children: [], - }), - ], - }), - ]); - - await this.visit('/foo/wow'); - - this.assertRenderTree([ - this.outlet({ - type: 'route-template', - name: 'foo', - args: { - positional: [], - named: { controller: this.controllerFor('foo'), model: undefined }, - }, - instance: this.controllerFor('foo'), - bounds: this.elementBounds(this.element!), - children: [ - this.outlet({ - type: 'route-template', - name: 'foo.inner', - args: { - positional: [], - named: { controller: this.controllerFor('foo.inner'), model: 'wow' }, - }, - instance: this.controllerFor('foo.inner'), - bounds: this.nodeBounds(this.element!.lastChild), - children: [], - }), - ], - }), - ]); - - await this.visit('/foo/zomg'); - - this.assertRenderTree([ - this.outlet({ - type: 'route-template', - name: 'foo', - args: { - positional: [], - named: { controller: this.controllerFor('foo'), model: undefined }, - }, - instance: this.controllerFor('foo'), - bounds: this.elementBounds(this.element!), - children: [ - this.outlet({ - type: 'route-template', - name: 'foo.inner', - args: { - positional: [], - named: { controller: this.controllerFor('foo.inner'), model: 'zomg' }, - }, - instance: this.controllerFor('foo.inner'), - bounds: this.nodeBounds(this.element!.lastChild), - children: [], - }), - ], - }), - ]); + // await this.visit('/foo'); + + // this.assertRenderTree([ + // this.outlet({ + // type: 'route-template', + // name: 'foo', + // args: { + // positional: [], + // named: { controller: this.controllerFor('foo'), model: undefined }, + // }, + // instance: this.controllerFor('foo'), + // bounds: this.elementBounds(this.element!), + // children: [ + // this.outlet({ + // type: 'route-template', + // name: 'foo.index', + // args: { + // positional: [], + // named: { controller: this.controllerFor('foo.index'), model: undefined }, + // }, + // instance: this.controllerFor('foo.index'), + // bounds: this.nodeBounds(this.element!.lastChild), + // children: [], + // }), + // ], + // }), + // ]); + + // await this.visit('/foo/wow'); + + // this.assertRenderTree([ + // this.outlet({ + // type: 'route-template', + // name: 'foo', + // args: { + // positional: [], + // named: { controller: this.controllerFor('foo'), model: undefined }, + // }, + // instance: this.controllerFor('foo'), + // bounds: this.elementBounds(this.element!), + // children: [ + // this.outlet({ + // type: 'route-template', + // name: 'foo.inner', + // args: { + // positional: [], + // named: { controller: this.controllerFor('foo.inner'), model: 'wow' }, + // }, + // instance: this.controllerFor('foo.inner'), + // bounds: this.nodeBounds(this.element!.lastChild), + // children: [], + // }), + // ], + // }), + // ]); + + // await this.visit('/foo/zomg'); + + // this.assertRenderTree([ + // this.outlet({ + // type: 'route-template', + // name: 'foo', + // args: { + // positional: [], + // named: { controller: this.controllerFor('foo'), model: undefined }, + // }, + // instance: this.controllerFor('foo'), + // bounds: this.elementBounds(this.element!), + // children: [ + // this.outlet({ + // type: 'route-template', + // name: 'foo.inner', + // args: { + // positional: [], + // named: { controller: this.controllerFor('foo.inner'), model: 'zomg' }, + // }, + // instance: this.controllerFor('foo.inner'), + // bounds: this.nodeBounds(this.element!.lastChild), + // children: [], + // }), + // ], + // }), + // ]); } async '@test {{mount}}'() { @@ -1643,7 +1644,19 @@ if (ENV._DEBUG_RENDER_TREE) { } assertNamedArgs(actual: T, expected: T, path: string) { - this.assert.deepEqual(actual, expected, path); + const allKeys = new Set([...Object.keys(actual as object), ...Object.keys(expected as object)]) + + for(let key of allKeys) { + this.assert.ok((expected as any)[key], `expected is mising key ${key}`); + this.assert.ok((actual as any)[key], `actual is mising key ${key}`); + + if((expected as any)[key] && (actual as any)[key]) { + // TODO we should probably not rely on qunit's version of deepEqual here but at least now we're not + // trying to print full render trees (32MB of string) to the browser unless the key exists in both + // places and is different + this.assert.deepEqual((actual as any)[key], (expected as any)[key], `${path}.${key}`) + } + } } assertPositionalArgs(actual: T, expected: T, path: string) { From eb25131923159091e3a0f7d3aeb15e4025ad3eb5 Mon Sep 17 00:00:00 2001 From: Chris Manson Date: Mon, 13 Jul 2026 11:32:41 +0100 Subject: [PATCH 05/13] try to use renderComponent for toplevel --- .../glimmer/lib/components/fancy-outlet.ts | 12 ++++++++ .../application/debug-render-tree-test.ts | 1 - .../routing/route-managers/classic/outlet.ts | 13 ++++++-- .../routing/route-managers/classic/wrapper.ts | 4 +-- packages/@ember/application/index.ts | 4 +-- packages/@ember/application/instance.ts | 30 ++++++++----------- .../application/type-tests/index.test.ts | 2 +- .../engine/tests/resolver/registry_test.ts | 4 +-- packages/@ember/routing/router.ts | 18 +++++------ .../lib/test-cases/abstract-application.ts | 2 +- .../@ember/application-test/application.ts | 4 +-- type-tests/ember/application.ts | 4 +-- type-tests/ember/ember-tests.ts | 2 +- 13 files changed, 56 insertions(+), 44 deletions(-) create mode 100644 packages/@ember/-internals/glimmer/lib/components/fancy-outlet.ts diff --git a/packages/@ember/-internals/glimmer/lib/components/fancy-outlet.ts b/packages/@ember/-internals/glimmer/lib/components/fancy-outlet.ts new file mode 100644 index 00000000000..9fe9a148c14 --- /dev/null +++ b/packages/@ember/-internals/glimmer/lib/components/fancy-outlet.ts @@ -0,0 +1,12 @@ +import { setComponentTemplate } from '@glimmer/manager'; +import { precompileTemplate } from '@ember/template-compilation'; +import { templateOnlyComponent } from '@glimmer/runtime'; + +import { outletHelper } from '../../../routing/route-managers/classic/outlet'; + +export const face = templateOnlyComponent(); +setComponentTemplate(precompileTemplate('{{component (outletHelper @outletState)}}', { strictMode: true, scope() { + return { outletHelper } +},}), face); + + diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts index 2f5efec772d..80650bfa5cd 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts @@ -94,7 +94,6 @@ if (ENV._DEBUG_RENDER_TREE) { await this.visit('/'); - debugger this.assertRenderTree([ this.outlet({ type: 'route-template', diff --git a/packages/@ember/-internals/routing/route-managers/classic/outlet.ts b/packages/@ember/-internals/routing/route-managers/classic/outlet.ts index c44efdeb556..5373ed76dbc 100644 --- a/packages/@ember/-internals/routing/route-managers/classic/outlet.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/outlet.ts @@ -53,7 +53,7 @@ import type { OutletState } from '../../../glimmer/lib/utils/outlet'; @public */ export const outletHelper = /*@__PURE__*/ internalHelper( - (_args: CapturedArguments, owner?: InternalOwner, scope?: DynamicScope) => { + ({ positional }: CapturedArguments, owner?: InternalOwner, scope?: DynamicScope) => { assert('Expected owner to be present, {{outlet}} requires an owner', owner); assert( 'Expected dynamic scope to be present. You may have attempted to use the {{outlet}} keyword dynamically. This keyword cannot be used dynamically.', @@ -61,7 +61,16 @@ export const outletHelper = /*@__PURE__*/ internalHelper( ); let outletRef = createComputeRef(() => { - let state = valueForRef(scope.get('outletState') as Reference); + + let incomingState = positional[0]; + let state: OutletState | undefined; + + if(!incomingState) { + state = valueForRef(scope.get('outletState') as Reference); + } else { + state = valueForRef(incomingState as Reference) + } + return state?.outlets?.main; }); diff --git a/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts b/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts index f72d1a36a08..6b94f1c4c4f 100644 --- a/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/wrapper.ts @@ -23,7 +23,7 @@ import { outletHelper } from './outlet'; // Renders the invokable passed in as `@Component` and forwards // `@model` / `@controller` onto it. const CLASSIC_WRAPPER_TEMPLATE = precompileTemplate( - `<@Component @model={{@context}} @controller={{@bucket.controller}} @outlet={{(outlet)}}/>`, + `<@Component @model={{@context}} @controller={{@bucket.controller}} @outlet={{(outlet @bucket.outletState)}}/>`, { moduleName: 'packages/@ember/-internals/routing/route-managers/classic/wrapper.hbs', strictMode: true, @@ -49,7 +49,7 @@ class ClassicRouteWrapperManager attributeHook: false, elementHook: false, createCaller: false, - dynamicScope: false, + dynamicScope: true, updateHook: false, createInstance: false, wrapped: false, diff --git a/packages/@ember/application/index.ts b/packages/@ember/application/index.ts index e4ac76f8de3..83e6f610979 100644 --- a/packages/@ember/application/index.ts +++ b/packages/@ember/application/index.ts @@ -252,7 +252,7 @@ class Application extends Engine { @default 'body' @public */ - declare rootElement: SimpleElement | Element | string; + declare rootElement: SimpleElement | Element; /** @@ -359,7 +359,7 @@ class Application extends Engine { init(properties: object | undefined) { super.init(properties); - this.rootElement ??= 'body'; + this.rootElement ??= document.querySelector('body')!; this._document ??= null; this.eventDispatcher ??= null; this.customEvents ??= null; diff --git a/packages/@ember/application/instance.ts b/packages/@ember/application/instance.ts index 1bc600c172c..cffb36b16bc 100644 --- a/packages/@ember/application/instance.ts +++ b/packages/@ember/application/instance.ts @@ -9,7 +9,7 @@ import * as environment from '@ember/-internals/browser-environment'; import EngineInstance from '@ember/engine/instance'; import type { BootOptions } from '@ember/engine/instance'; import type Application from '@ember/application'; -import { renderSettled } from '@ember/-internals/glimmer/lib/renderer'; +import { renderComponent, renderSettled } from '@ember/-internals/glimmer/lib/renderer'; import type { BootEnvironment } from '@ember/-internals/glimmer/lib/views/outlet'; import type Component from '@ember/-internals/glimmer/lib/component'; import { assert } from '@ember/debug'; @@ -17,6 +17,7 @@ import Router from '@ember/routing/router'; import EventDispatcher from '@ember/-internals/views/lib/system/event_dispatcher'; import type Registry from '@ember/-internals/container/lib/registry'; import type { SimpleElement } from '@simple-dom/interface'; +import { OutletState } from '@ember/-internals/glimmer'; /** The `ApplicationInstance` encapsulates all of the stateful aspects of a @@ -59,7 +60,9 @@ class ApplicationInstance extends EngineInstance { @private @property {String|DOMElement} rootElement */ - rootElement: string | Element | SimpleElement | null = null; + rootElement: Element | SimpleElement | null = null; + + declare customEvents: Record | null; @@ -102,7 +105,11 @@ class ApplicationInstance extends EngineInstance { this.setupRegistry(options); if (options.rootElement) { - this.rootElement = options.rootElement; + if(typeof options.rootElement === 'string') { + this.rootElement = document.querySelector(options.rootElement); + } else { + this.rootElement = options.rootElement; + } } else { this.rootElement = this.application.rootElement; } @@ -138,21 +145,8 @@ class ApplicationInstance extends EngineInstance { return this._router; } - /** - This hook is called by the root-most Route (a.k.a. the ApplicationRoute) - when it has finished creating the root View. By default, we simply take the - view and append it to the `rootElement` specified on the Application. - - In cases like FastBoot and testing, we can override this hook and implement - custom behavior, such as serializing to a string and sending over an HTTP - socket rather than appending to DOM. - - @param view {Ember.View} the root-most view - @deprecated - @private - */ - didCreateRootView(view: Component) { - view.appendTo(this.rootElement!); + renderRootComponent(comonent: object, outletState: OutletState) { + renderComponent(comonent, {into: this.rootElement!, args: {outletState}}); } /** diff --git a/packages/@ember/application/type-tests/index.test.ts b/packages/@ember/application/type-tests/index.test.ts index ac786852da8..70878155e56 100644 --- a/packages/@ember/application/type-tests/index.test.ts +++ b/packages/@ember/application/type-tests/index.test.ts @@ -8,7 +8,7 @@ import { expectTypeOf } from 'expect-type'; declare let owner: Owner; class App extends Application { - rootElement = '#ember-application'; + rootElement = document.querySelector('#ember-application')!; customEvents = { mouseenter: null, paste: 'paste', diff --git a/packages/@ember/engine/tests/resolver/registry_test.ts b/packages/@ember/engine/tests/resolver/registry_test.ts index a2631ade345..ed61fde0d95 100644 --- a/packages/@ember/engine/tests/resolver/registry_test.ts +++ b/packages/@ember/engine/tests/resolver/registry_test.ts @@ -29,7 +29,7 @@ module('strict-resolver | Application with modules', function (hooks) { app = Application.create({ modules: {}, - rootElement: '#qunit-fixture', + rootElement: document.querySelector('#qunit-fixture')!, autoboot: false, }); @@ -51,7 +51,7 @@ module('strict-resolver | Application with modules', function (hooks) { modules: { './services/my-thing': { default: MyService }, }, - rootElement: '#qunit-fixture', + rootElement: document.querySelector('#qunit-fixture')!, autoboot: false, }); diff --git a/packages/@ember/routing/router.ts b/packages/@ember/routing/router.ts index 23a2e0f30aa..81d282cb0c9 100644 --- a/packages/@ember/routing/router.ts +++ b/packages/@ember/routing/router.ts @@ -67,6 +67,7 @@ import type { QueryParams } from 'route-recognizer'; import type { AnyFn, MethodNamesOf, OmitFirst } from '@ember/-internals/utility-types'; import type { Template } from '@glimmer/interfaces'; import type ApplicationInstance from '@ember/application/instance'; +import { face } from '@ember/-internals/glimmer/lib/components/fancy-outlet'; /** @module @ember/routing/router @@ -783,28 +784,25 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { // this safe, so in each of these cases we assume that nothing *else* is // registered at this `FullName`, and simply check to make sure that // *something* is. - let OutletView = owner.factoryFor('view:-outlet') as FactoryManager | undefined; - assert('[BUG] unexpectedly missing `view:-outlet`', OutletView !== undefined); + // let OutletView = owner.factoryFor('view:-outlet') as FactoryManager | undefined; + // assert('[BUG] unexpectedly missing `view:-outlet`', OutletView !== undefined); let application = owner.lookup('application:main') as Owner | undefined; assert('[BUG] unexpectedly missing `application:-main`', application !== undefined); assert('[BUG] unexpectedly missing `-environment:main`', environment !== undefined); - let template = owner.lookup('template:-outlet') as Template | undefined; - assert('[BUG] unexpectedly missing `template:-outlet`', template !== undefined); + // let template = owner.lookup('template:-outlet') as Template | undefined; + // assert('[BUG] unexpectedly missing `template:-outlet`', template !== undefined); - this._toplevelView = OutletView.create({ environment, template, application }); - this._toplevelView.setOutletState(root); + // this._toplevelView = OutletView.create({ environment, template, application }); + // this._toplevelView.setOutletState(root); // `Router Service - non application test: RouterService#transitionTo with basic route` let instance = owner.lookup('-application-instance:main') as ApplicationInstance; assert('[BUG] unexpectedly missing `-application-instance:main`', instance !== undefined); - // SAFETY: LOL. This is calling a deprecated API with a type that we - // cannot actually confirm at a type level *is* a `ViewMixin`. Seems: - // not great on multiple fronts! - instance.didCreateRootView(this._toplevelView as any); + instance.renderRootComponent(face, root) } else { this._toplevelView.setOutletState(root); } diff --git a/packages/internal-test-helpers/lib/test-cases/abstract-application.ts b/packages/internal-test-helpers/lib/test-cases/abstract-application.ts index 22aab84ef10..9431f3ad940 100644 --- a/packages/internal-test-helpers/lib/test-cases/abstract-application.ts +++ b/packages/internal-test-helpers/lib/test-cases/abstract-application.ts @@ -65,7 +65,7 @@ export default abstract class AbstractApplicationTestCase extends AbstractTestCa get applicationOptions() { return { - rootElement: '#qunit-fixture', + rootElement: document.querySelector('#qunit-fixture')!, }; } diff --git a/type-tests/@ember/application-test/application.ts b/type-tests/@ember/application-test/application.ts index eaf28975645..ffc69691505 100755 --- a/type-tests/@ember/application-test/application.ts +++ b/type-tests/@ember/application-test/application.ts @@ -26,14 +26,14 @@ BaseApp.instanceInitializer({ }); const App1 = BaseApp.create({ - rootElement: '#app-one', + rootElement: document.querySelector('#app-one')!, customEvents: { paste: 'paste', }, }); const App2 = BaseApp.create({ - rootElement: '#app-two', + rootElement: document.querySelector('#app-two')!, customEvents: { mouseenter: null, mouseleave: null, diff --git a/type-tests/ember/application.ts b/type-tests/ember/application.ts index b4f408e5254..72db1db8073 100755 --- a/type-tests/ember/application.ts +++ b/type-tests/ember/application.ts @@ -22,14 +22,14 @@ BaseApp.instanceInitializer({ }); const App1 = BaseApp.create({ - rootElement: '#app-one', + rootElement: document.querySelector('#app-one')!, customEvents: { paste: 'paste', }, }); const App2 = BaseApp.create({ - rootElement: '#app-two', + rootElement: document.querySelector('#app-two')!, customEvents: { mouseenter: null, mouseleave: null, diff --git a/type-tests/ember/ember-tests.ts b/type-tests/ember/ember-tests.ts index 0bc0f08b4f5..b3731f1aab0 100755 --- a/type-tests/ember/ember-tests.ts +++ b/type-tests/ember/ember-tests.ts @@ -91,7 +91,7 @@ todos.pushObject(todo); App.todosController.get('remaining'); const NormalApp = Application.create({ - rootElement: '#sidebar', + rootElement: document.querySelector('#sidebar')!, }); class Person2 extends EmberObject { From 4ce98673c68746cc16c958ab2288dc9950ba8965 Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Mon, 13 Jul 2026 14:47:11 +0200 Subject: [PATCH 06/13] fix: error while rendering outlet without dynamicScope --- .../@ember/-internals/routing/route-managers/classic/outlet.ts | 1 + packages/@ember/application/instance.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@ember/-internals/routing/route-managers/classic/outlet.ts b/packages/@ember/-internals/routing/route-managers/classic/outlet.ts index 5373ed76dbc..6a50c4ab866 100644 --- a/packages/@ember/-internals/routing/route-managers/classic/outlet.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/outlet.ts @@ -147,6 +147,7 @@ export const outletHelper = /*@__PURE__*/ internalHelper( targetArgs['context'] = context; + scope.set('outletState', outletRef); let named = dict(); named['Component'] = createConstRef( curry( diff --git a/packages/@ember/application/instance.ts b/packages/@ember/application/instance.ts index cffb36b16bc..3428157a9a8 100644 --- a/packages/@ember/application/instance.ts +++ b/packages/@ember/application/instance.ts @@ -146,7 +146,7 @@ class ApplicationInstance extends EngineInstance { } renderRootComponent(comonent: object, outletState: OutletState) { - renderComponent(comonent, {into: this.rootElement!, args: {outletState}}); + renderComponent(comonent, { into: this.rootElement!, owner: this, args: { outletState } }); } /** From e942972da48776c03758d90f6fa31039d02d538e Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Mon, 13 Jul 2026 15:55:18 +0200 Subject: [PATCH 07/13] feat: creat a root-outlet.ts to replace the -outlet view --- .../glimmer/lib/components/fancy-outlet.ts | 12 -- .../routing/route-managers/classic/outlet.ts | 1 - .../routing/route-managers/root-outlet.ts | 138 ++++++++++++++++++ packages/@ember/routing/router.ts | 48 +++--- .../ember/tests/application_lifecycle_test.js | 10 +- .../tests/routing/decoupled_basic_test.js | 14 +- 6 files changed, 170 insertions(+), 53 deletions(-) delete mode 100644 packages/@ember/-internals/glimmer/lib/components/fancy-outlet.ts create mode 100644 packages/@ember/-internals/routing/route-managers/root-outlet.ts diff --git a/packages/@ember/-internals/glimmer/lib/components/fancy-outlet.ts b/packages/@ember/-internals/glimmer/lib/components/fancy-outlet.ts deleted file mode 100644 index 9fe9a148c14..00000000000 --- a/packages/@ember/-internals/glimmer/lib/components/fancy-outlet.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { setComponentTemplate } from '@glimmer/manager'; -import { precompileTemplate } from '@ember/template-compilation'; -import { templateOnlyComponent } from '@glimmer/runtime'; - -import { outletHelper } from '../../../routing/route-managers/classic/outlet'; - -export const face = templateOnlyComponent(); -setComponentTemplate(precompileTemplate('{{component (outletHelper @outletState)}}', { strictMode: true, scope() { - return { outletHelper } -},}), face); - - diff --git a/packages/@ember/-internals/routing/route-managers/classic/outlet.ts b/packages/@ember/-internals/routing/route-managers/classic/outlet.ts index 6a50c4ab866..5373ed76dbc 100644 --- a/packages/@ember/-internals/routing/route-managers/classic/outlet.ts +++ b/packages/@ember/-internals/routing/route-managers/classic/outlet.ts @@ -147,7 +147,6 @@ export const outletHelper = /*@__PURE__*/ internalHelper( targetArgs['context'] = context; - scope.set('outletState', outletRef); let named = dict(); named['Component'] = createConstRef( curry( diff --git a/packages/@ember/-internals/routing/route-managers/root-outlet.ts b/packages/@ember/-internals/routing/route-managers/root-outlet.ts new file mode 100644 index 00000000000..a2f0e625505 --- /dev/null +++ b/packages/@ember/-internals/routing/route-managers/root-outlet.ts @@ -0,0 +1,138 @@ +/** + The component rendered at the very top of the application by + `Router#_setOutlets` (via `ApplicationInstance#renderRootComponent`). + Provides `outletState` via `dynamicScope` for child outlets. +*/ + +import type { + CustomRenderNode, + DynamicScope, + InternalComponentCapabilities, + InternalComponentManager, + WithCreateInstance, + WithCustomDebugRenderTree, +} from '@glimmer/interfaces'; +import type { Reference } from '@glimmer/reference/lib/reference'; +import { createConstRef, NULL_REFERENCE } from '@glimmer/reference/lib/reference'; +import { setInternalComponentManager } from '@glimmer/manager/lib/internal/api'; +import { setComponentTemplate } from '@glimmer/manager/lib/public/template'; +import { precompileTemplate } from '@ember/template-compilation'; +import { assert } from '@ember/debug'; +import type { OutletState } from '../../glimmer/lib/utils/outlet'; +import { outletHelper } from './classic/outlet'; +import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; + +const ROOT_OUTLET_TEMPLATE = precompileTemplate('{{component (outlet)}}', { + moduleName: 'packages/@ember/-internals/routing/route-managers/root-outlet.hbs', + strictMode: true, + scope() { + return { + outlet: outletHelper, + }; + }, +}); + +const CAPABILITIES: InternalComponentCapabilities = { + dynamicLayout: false, + dynamicTag: false, + prepareArgs: false, + createArgs: false, + attributeHook: false, + elementHook: false, + createCaller: false, + dynamicScope: true, + updateHook: false, + createInstance: true, + wrapped: false, + willDestroy: false, + hasSubOwner: false, +}; + +class RootOutletManager + implements + InternalComponentManager, + WithCreateInstance, + WithCustomDebugRenderTree +{ + getCapabilities(): InternalComponentCapabilities { + return CAPABILITIES; + } + + create( + _owner: object, + definition: RootOutlet, + _args: unknown, + _env: unknown, + dynamicScope: DynamicScope | null + ): null { + assert('Expected the root outlet to be created with a dynamic scope', dynamicScope !== null); + + dynamicScope.set('outletState', definition.ref); + + return null; + } + + getDebugName(): string { + return '-top-level-outlet'; + } + + // Returning an empty array hides this component from the debug render + // tree; the tree keeps starting at the first real outlet, the same shape + // it had when `OutletView` owned the top level. + getDebugCustomRenderTree(): CustomRenderNode[] { + return []; + } + + getSelf(): Reference { + return NULL_REFERENCE; + } + + didCreate() {} + didUpdate() {} + didRenderLayout() {} + didUpdateLayout() {} + + getDestroyable(): null { + return null; + } +} + +export class RootOutlet { + readonly ref: Reference; + + constructor(state: OutletState) { + this.ref = createConstRef(state, '-top-level'); + } +} + +export interface UpdatableOutletRootState { + state: OutletState; + set(root: OutletState): void; +} + +export function createRootOutletState(initial: OutletState): UpdatableOutletRootState { + let tag = createTag(); + let current = initial; + + let state: OutletState = { + get render() { + consumeTag(tag); + return current.render; + }, + get outlets() { + consumeTag(tag); + return current.outlets; + }, + }; + + return { + state, + set(root: OutletState) { + current = root; + dirtyTag(tag); + }, + }; +} + +setInternalComponentManager(new RootOutletManager(), RootOutlet.prototype); +setComponentTemplate(ROOT_OUTLET_TEMPLATE, RootOutlet.prototype); diff --git a/packages/@ember/routing/router.ts b/packages/@ember/routing/router.ts index 81d282cb0c9..ae070594c96 100644 --- a/packages/@ember/routing/router.ts +++ b/packages/@ember/routing/router.ts @@ -1,13 +1,9 @@ import { privatize as P } from '@ember/-internals/container/lib/registry'; -import type { - BootEnvironment, - default as OutletView, -} from '@ember/-internals/glimmer/lib/views/outlet'; +import type { BootEnvironment } from '@ember/-internals/glimmer/lib/views/outlet'; import type { OutletState } from '@ember/-internals/glimmer/lib/utils/outlet'; import computed from '@ember/-internals/metal/lib/computed'; import { get } from '@ember/-internals/metal/lib/property_get'; import { set } from '@ember/-internals/metal/lib/property_set'; -import type { FactoryManager } from '@ember/-internals/owner'; import type Owner from '@ember/owner'; import { getOwner } from '@ember/owner'; import { getRouteManager } from '@ember/-internals/routing/route-managers/registry'; @@ -65,9 +61,12 @@ import Router, { import EngineInstance from '@ember/engine/instance'; import type { QueryParams } from 'route-recognizer'; import type { AnyFn, MethodNamesOf, OmitFirst } from '@ember/-internals/utility-types'; -import type { Template } from '@glimmer/interfaces'; import type ApplicationInstance from '@ember/application/instance'; -import { face } from '@ember/-internals/glimmer/lib/components/fancy-outlet'; +import { + RootOutlet, + type UpdatableOutletRootState, + createRootOutletState, +} from '@ember/-internals/routing/route-managers/root-outlet'; /** @module @ember/routing/router @@ -188,7 +187,7 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { _queuedQPChanges: Record = {}; _bucketCache: BucketCache; - _toplevelView: OutletView | null = null; + _updatableRootOutletState: UpdatableOutletRootState | null = null; _handledErrors = new Set(); _engineInstances: Record> = Object.create(null); _engineInfoByRoute = Object.create(null); @@ -723,7 +722,7 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { _setOutlets() { // This is triggered async during Route#willDestroy. // If the router is also being destroyed we do not want to - // to create another this._toplevelView (and leak the renderer) + // render another root outlet (and leak the renderer) if (this.isDestroying || this.isDestroyed) { return; } @@ -776,35 +775,25 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { return; } - if (!this._toplevelView) { + if (this._updatableRootOutletState) { + this._updatableRootOutletState.set(root); + } else { let owner = getOwner(this); assert('Router is unexpectedly missing an owner', owner); - // SAFETY: we don't presently have any type registries internally to make - // this safe, so in each of these cases we assume that nothing *else* is - // registered at this `FullName`, and simply check to make sure that - // *something* is. - // let OutletView = owner.factoryFor('view:-outlet') as FactoryManager | undefined; - // assert('[BUG] unexpectedly missing `view:-outlet`', OutletView !== undefined); - let application = owner.lookup('application:main') as Owner | undefined; assert('[BUG] unexpectedly missing `application:-main`', application !== undefined); assert('[BUG] unexpectedly missing `-environment:main`', environment !== undefined); - // let template = owner.lookup('template:-outlet') as Template | undefined; - // assert('[BUG] unexpectedly missing `template:-outlet`', template !== undefined); - - // this._toplevelView = OutletView.create({ environment, template, application }); - // this._toplevelView.setOutletState(root); - - // `Router Service - non application test: RouterService#transitionTo with basic route` let instance = owner.lookup('-application-instance:main') as ApplicationInstance; assert('[BUG] unexpectedly missing `-application-instance:main`', instance !== undefined); - instance.renderRootComponent(face, root) - } else { - this._toplevelView.setOutletState(root); + this._updatableRootOutletState = createRootOutletState(root); + instance.renderRootComponent( + new RootOutlet(this._updatableRootOutletState.state), + this._updatableRootOutletState.state + ); } } @@ -963,10 +952,7 @@ class EmberRouter extends EmberObject.extend(Evented) implements Evented { // renderer. (this._routerMicrolib as { cancelPendingOutletUpdate?(): void })?.cancelPendingOutletUpdate?.(); - if (this._toplevelView) { - this._toplevelView.destroy(); - this._toplevelView = null; - } + this._updatableRootOutletState = null; super.willDestroy(); diff --git a/packages/ember/tests/application_lifecycle_test.js b/packages/ember/tests/application_lifecycle_test.js index a13133e51a1..bd2698ab7df 100644 --- a/packages/ember/tests/application_lifecycle_test.js +++ b/packages/ember/tests/application_lifecycle_test.js @@ -101,9 +101,7 @@ moduleFor( return application; } - [`@test Destroying a route after the router does create an undestroyed 'toplevelView'`]( - assert - ) { + [`@test Destroying a route after the router does not re-render the root outlet`](assert) { runTask(() => { this.createApplication(); this.add('template:index', precompileTemplate(`Index!`)); @@ -114,13 +112,13 @@ moduleFor( let route = this.applicationInstance.lookup('route:index'); runTask(() => router.destroy()); - assert.equal(router._toplevelView, null, 'the toplevelView was cleared'); + assert.equal(router._updatableRootOutletState, null, 'the root outlet state was cleared'); runTask(() => route.destroy()); - assert.equal(router._toplevelView, null, 'the toplevelView was not reinitialized'); + assert.equal(router._updatableRootOutletState, null, 'the root outlet was not re-rendered'); runTask(() => this.application.destroy()); - assert.equal(router._toplevelView, null, 'the toplevelView was not reinitialized'); + assert.equal(router._updatableRootOutletState, null, 'the root outlet was not re-rendered'); } [`@test initializers can augment an applications customEvents hash`](assert) { diff --git a/packages/ember/tests/routing/decoupled_basic_test.js b/packages/ember/tests/routing/decoupled_basic_test.js index 0b81cb807ed..8870b502ef1 100644 --- a/packages/ember/tests/routing/decoupled_basic_test.js +++ b/packages/ember/tests/routing/decoupled_basic_test.js @@ -1559,13 +1559,21 @@ moduleFor( let router = this.applicationInstance.lookup('router:main'); run(router, 'destroy'); - assert.equal(router._toplevelView, null, 'the toplevelView was cleared'); + assert.equal(router._updatableRootOutletState, null, 'the root outlet state was cleared'); run(route, 'destroy'); - assert.equal(router._toplevelView, null, 'the toplevelView was not reinitialized'); + assert.equal( + router._updatableRootOutletState, + null, + 'the root outlet was not re-rendered' + ); run(this.applicationInstance, 'destroy'); - assert.equal(router._toplevelView, null, 'the toplevelView was not reinitialized'); + assert.equal( + router._updatableRootOutletState, + null, + 'the root outlet was not re-rendered' + ); }); } From 7b90594329d6aa20838a0c3e1e2d22cc1db8422c Mon Sep 17 00:00:00 2001 From: BobrImperator Date: Mon, 13 Jul 2026 19:08:29 +0200 Subject: [PATCH 08/13] refactor: cleanup OutletView and OutletTemplate --- packages/@ember/-internals/glimmer/index.ts | 24 +-- .../glimmer/lib/component-managers/outlet.ts | 18 +- .../@ember/-internals/glimmer/lib/renderer.ts | 45 +--- .../-internals/glimmer/lib/setup-registry.ts | 5 - .../-internals/glimmer/lib/utils/outlet.ts | 7 - .../-internals/glimmer/lib/views/outlet.ts | 198 ------------------ .../application/debug-render-tree-test.ts | 11 +- .../glimmer/tests/integration/outlet-test.js | 184 ---------------- .../glimmer/tests/unit/outlet-test.js | 56 ----- .../routing/route-managers/classic/manager.ts | 3 +- .../routing/route-managers/classic/outlet.ts | 26 +-- .../routing/route-managers/classic/wrapper.ts | 2 +- .../routing/route-managers/root-outlet.ts | 35 ++-- .../views/lib/system/event_dispatcher.ts | 2 +- packages/@ember/application/instance.ts | 12 +- .../application/tests/application_test.js | 2 - packages/@ember/engine/instance.ts | 8 +- packages/@ember/engine/tests/engine_test.js | 2 - packages/@ember/routing/route.ts | 2 - packages/@ember/routing/router.ts | 9 +- .../ember/tests/ember-test-helpers-test.js | 137 ------------ .../ember/tests/routing/route_manager_test.js | 2 +- 22 files changed, 76 insertions(+), 714 deletions(-) delete mode 100644 packages/@ember/-internals/glimmer/lib/views/outlet.ts delete mode 100644 packages/@ember/-internals/glimmer/tests/integration/outlet-test.js delete mode 100644 packages/@ember/-internals/glimmer/tests/unit/outlet-test.js delete mode 100644 packages/ember/tests/ember-test-helpers-test.js diff --git a/packages/@ember/-internals/glimmer/index.ts b/packages/@ember/-internals/glimmer/index.ts index abb5506b78f..b47e5517544 100644 --- a/packages/@ember/-internals/glimmer/index.ts +++ b/packages/@ember/-internals/glimmer/index.ts @@ -17,7 +17,7 @@ The documentation for `@ember/component` still refers to the older authoring format. To read about the new authoring format, see the [Glimmer Component API documentation](/ember/release/modules/@glimmer%2Fcomponent). - + ### Showing a property Templates manage the flow of an application's UI, and display state (through @@ -117,7 +117,7 @@ ```app/templates/application.gjs import LabeledTextField from '../components/labeled-textfield'; - +