Skip to content

Commit

Permalink
[FEATURE] Allow components in routes
Browse files Browse the repository at this point in the history
Second attempt at #20768

* Refactor to make component the primary path, normalizes templates
  into a custom component with `{{this}}` set to controller
* Keep the core `{{outlet}}` responsibilities separate from what
  goes into the outlet
* Ensures proper debug render tree output – `route-template` node
  only appears when a template (the custom component) is used
* Ensures this works with test-helpers & others that
* It doesn't differentiate between components and templates once
  normalized so `@controller` and `@model` is passed to both

TODO:

* Feature flag
* Test and integrate this in @ember/test-helpers + smoke test
* Inlined some TODO comments to investigate/simplify things

Co-authored-by: Edward Faulkner <[email protected]>
  • Loading branch information
chancancode and ef4 committed Nov 22, 2024
1 parent d58ceec commit 1a16865
Show file tree
Hide file tree
Showing 21 changed files with 751 additions and 125 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ module.exports = {

'disable-features/disable-async-await': 'error',
'disable-features/disable-generator-functions': 'error',
'import/no-unresolved': [
'error',
{
ignore: ['@ember/template-compiler'],
},
],
},

settings: {
Expand Down
109 changes: 48 additions & 61 deletions packages/@ember/-internals/glimmer/lib/component-managers/outlet.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import type { InternalOwner } from '@ember/-internals/owner';
import type { Nullable } from '@ember/-internals/utility-types';
import { assert } from '@ember/debug';
import EngineInstance from '@ember/engine/instance';
import { _instrumentStart } from '@ember/instrumentation';
import { precompileTemplate } from '@ember/template-compilation';
import type {
CapturedArguments,
CompilableProgram,
ComponentDefinition,
CapabilityMask,
CustomRenderNode,
Destroyable,
Environment,
InternalComponentCapabilities,
Template,
VMArguments,
WithCreateInstance,
WithCustomDebugRenderTree,
} from '@glimmer/interfaces';
import type { Nullable } from '@ember/-internals/utility-types';
import { capabilityFlagsFrom } from '@glimmer/manager';
import type { Reference } from '@glimmer/reference';
import { createConstRef, valueForRef } from '@glimmer/reference';
import { UNDEFINED_REFERENCE, valueForRef } from '@glimmer/reference';
import { EMPTY_ARGS } from '@glimmer/runtime';
import { unwrapTemplate } from '@glimmer/util';

Expand All @@ -33,19 +31,18 @@ function instrumentationPayload(def: OutletDefinitionState) {
}

interface OutletInstanceState {
self: Reference;
outletBucket?: {};
engineBucket?: { mountPoint: string };
engine?: EngineInstance;
engine?: {
instance: EngineInstance;
mountPoint: string;
};
finalize: () => void;
}

export interface OutletDefinitionState {
ref: Reference<OutletState | undefined>;
name: string;
template: Template;
template: object;
controller: unknown;
model: unknown;
}

const CAPABILITIES: InternalComponentCapabilities = {
Expand All @@ -64,9 +61,11 @@ const CAPABILITIES: InternalComponentCapabilities = {
hasSubOwner: false,
};

const CAPABILITIES_MASK = capabilityFlagsFrom(CAPABILITIES);

class OutletComponentManager
implements
WithCreateInstance<OutletInstanceState>,
WithCreateInstance<OutletInstanceState, OutletDefinitionState>,
WithCustomDebugRenderTree<OutletInstanceState, OutletDefinitionState>
{
create(
Expand All @@ -79,54 +78,54 @@ class OutletComponentManager
let parentStateRef = dynamicScope.get('outletState');
let currentStateRef = definition.ref;

// This is the actual primary responsibility of the outlet component –
// it represents the switching from one route component/template into
// the next. The rest only exists to support the debug render tree and
// the old-school (and unreliable) instrumentation.
dynamicScope.set('outletState', currentStateRef);

let state: OutletInstanceState = {
self: createConstRef(definition.controller, 'this'),
finalize: _instrumentStart('render.outlet', instrumentationPayload, definition),
};

if (env.debugRenderTree !== undefined) {
state.outletBucket = {};

let parentState = valueForRef(parentStateRef);
let parentOwner = parentState && parentState.render && parentState.render.owner;
let currentOwner = valueForRef(currentStateRef)!.render!.owner;
let parentOwner = parentState?.render?.owner;
let currentState = valueForRef(currentStateRef);
let currentOwner = currentState?.render?.owner;

if (parentOwner && parentOwner !== currentOwner) {
assert(
'Expected currentOwner to be an EngineInstance',
currentOwner instanceof EngineInstance
);

let mountPoint = currentOwner.mountPoint;

state.engine = currentOwner;
let { mountPoint } = currentOwner;

if (mountPoint) {
state.engineBucket = { mountPoint };
state.engine = {
mountPoint,
instance: currentOwner,
};
}
}
}

return state;
}

getDebugName({ name }: OutletDefinitionState) {
return name;
getDebugName({ name }: OutletDefinitionState): string {
return `{{outlet}} for ${name}`;
}

getDebugCustomRenderTree(
definition: OutletDefinitionState,
state: OutletInstanceState,
args: CapturedArguments
_definition: OutletDefinitionState,
state: OutletInstanceState
): CustomRenderNode[] {
let nodes: CustomRenderNode[] = [];

assert('[BUG] outletBucket must be set', state.outletBucket);

nodes.push({
bucket: state.outletBucket,
bucket: state,
type: 'outlet',
// "main" used to be the outlet name, keeping it around for compatibility
name: 'main',
Expand All @@ -135,35 +134,26 @@ class OutletComponentManager
template: undefined,
});

if (state.engineBucket) {
if (state.engine) {
nodes.push({
bucket: state.engineBucket,
bucket: state.engine,
type: 'engine',
name: state.engineBucket.mountPoint,
name: state.engine.mountPoint,
args: EMPTY_ARGS,
instance: state.engine,
instance: state.engine.instance,
template: undefined,
});
}

nodes.push({
bucket: state,
type: 'route-template',
name: definition.name,
args: args,
instance: definition.controller,
template: unwrapTemplate(definition.template).moduleName,
});

return nodes;
}

getCapabilities(): InternalComponentCapabilities {
return CAPABILITIES;
}

getSelf({ self }: OutletInstanceState) {
return self;
getSelf() {
return UNDEFINED_REFERENCE;
}

didCreate() {}
Expand All @@ -182,30 +172,27 @@ class OutletComponentManager

const OUTLET_MANAGER = new OutletComponentManager();

export class OutletComponentDefinition
const OUTLET_COMPONENT_TEMPLATE = precompileTemplate(
'<@Component @controller={{@controller}} @model={{@model}} />',
{ strictMode: true }
);

export class OutletComponent
implements
ComponentDefinition<OutletDefinitionState, OutletInstanceState, OutletComponentManager>
{
// handle is not used by this custom definition
public handle = -1;

public resolvedName: string;
public resolvedName = null;
public manager = OUTLET_MANAGER;
public capabilities = CAPABILITIES_MASK;
public compilable: CompilableProgram;
public capabilities: CapabilityMask;

constructor(
public state: OutletDefinitionState,
public manager: OutletComponentManager = OUTLET_MANAGER
) {
let capabilities = manager.getCapabilities();
this.capabilities = capabilityFlagsFrom(capabilities);
this.compilable = capabilities.wrapped
? unwrapTemplate(state.template).asWrappedLayout()
: unwrapTemplate(state.template).asLayout();
this.resolvedName = state.name;

constructor(owner: InternalOwner, public state: OutletDefinitionState) {
this.compilable = unwrapTemplate(OUTLET_COMPONENT_TEMPLATE(owner)).asLayout();
}
}

export function createRootOutlet(outletView: OutletView): OutletComponentDefinition {
return new OutletComponentDefinition(outletView.state);
export function createRootOutlet(outletView: OutletView): OutletComponent {
return new OutletComponent(outletView.owner, outletView.state);
}
Loading

0 comments on commit 1a16865

Please sign in to comment.