Skip to content

Commit 8ad077a

Browse files
committed
[FEATURE MODEL_ARG] Implement RFC emberjs/rfcs#523 for {{mount}}
This is an extension to the RFC not explicitly written in the RFC text. I missed this when writing the RFC, but we felt that `{{mount}}` with the `model=` argument is even more clearly an argument, and that we explicitly decided to restrict the `{{mount}}` syntax to a single `model` argument (as opposed to arbitrary named arguments), so it is within the spirit of the RFC to make this work also. This also refactor the implementation of `{{mount}}` to do less custom work (like diffing the tag) and let Glimmer VM do more of the work via the usual paths.
1 parent 49117d7 commit 8ad077a

File tree

4 files changed

+175
-70
lines changed

4 files changed

+175
-70
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { DEBUG } from '@glimmer/env';
22
import { ComponentCapabilities } from '@glimmer/interfaces';
3-
import { CONSTANT_TAG, Tag, validate, value, VersionedPathReference } from '@glimmer/reference';
4-
import { ComponentDefinition, Invocation, WithDynamicLayout } from '@glimmer/runtime';
3+
import { CONSTANT_TAG, Tag, VersionedPathReference } from '@glimmer/reference';
4+
import { Arguments, ComponentDefinition, Invocation, WithDynamicLayout } from '@glimmer/runtime';
55
import { Destroyable, Opaque, Option } from '@glimmer/util';
66

77
import { Owner } from '@ember/-internals/owner';
88
import { generateControllerFactory } from '@ember/-internals/routing';
99
import { OwnedTemplateMeta } from '@ember/-internals/views';
10+
import { EMBER_ROUTING_MODEL_ARG } from '@ember/canary-features';
11+
import { assert } from '@ember/debug';
12+
1013
import { TemplateFactory } from '../..';
1114
import Environment from '../environment';
1215
import RuntimeResolver from '../resolver';
@@ -23,24 +26,18 @@ interface EngineState {
2326
engine: EngineInstance;
2427
controller: any;
2528
self: RootReference<any>;
26-
tag: Tag;
27-
}
28-
29-
interface EngineWithModelState extends EngineState {
30-
modelRef: VersionedPathReference<Opaque>;
31-
modelRev: number;
29+
modelRef?: VersionedPathReference<Opaque>;
3230
}
3331

3432
interface EngineDefinitionState {
3533
name: string;
36-
modelRef: VersionedPathReference<Opaque> | undefined;
3734
}
3835

3936
const CAPABILITIES = {
4037
dynamicLayout: true,
4138
dynamicTag: false,
4239
prepareArgs: false,
43-
createArgs: false,
40+
createArgs: true,
4441
attributeHook: false,
4542
elementHook: false,
4643
createCaller: true,
@@ -49,10 +46,13 @@ const CAPABILITIES = {
4946
createInstance: true,
5047
};
5148

52-
class MountManager
53-
extends AbstractManager<EngineState | EngineWithModelState, EngineDefinitionState>
54-
implements
55-
WithDynamicLayout<EngineState | EngineWithModelState, OwnedTemplateMeta, RuntimeResolver> {
49+
// TODO
50+
// This "disables" the "@model" feature by making the arg untypable syntatically
51+
// Delete this when EMBER_ROUTING_MODEL_ARG has shipped
52+
export const MODEL_ARG_NAME = EMBER_ROUTING_MODEL_ARG || !DEBUG ? 'model' : ' untypable model arg ';
53+
54+
class MountManager extends AbstractManager<EngineState, EngineDefinitionState>
55+
implements WithDynamicLayout<EngineState, OwnedTemplateMeta, RuntimeResolver> {
5656
getDynamicLayout(state: EngineState, _: RuntimeResolver): Invocation {
5757
let templateFactory = state.engine.lookup('template:application') as TemplateFactory;
5858
let template = templateFactory(state.engine);
@@ -68,39 +68,40 @@ class MountManager
6868
return CAPABILITIES;
6969
}
7070

71-
create(environment: Environment, state: EngineDefinitionState) {
71+
create(environment: Environment, { name }: EngineDefinitionState, args: Arguments) {
7272
if (DEBUG) {
73-
this._pushEngineToDebugStack(`engine:${state.name}`, environment);
73+
this._pushEngineToDebugStack(`engine:${name}`, environment);
7474
}
7575

7676
// TODO
7777
// mount is a runtime helper, this shouldn't use dynamic layout
7878
// we should resolve the engine app template in the helper
7979
// it also should use the owner that looked up the mount helper.
8080

81-
let engine = environment.owner.buildChildEngineInstance<EngineInstance>(state.name);
81+
let engine = environment.owner.buildChildEngineInstance<EngineInstance>(name);
8282

8383
engine.boot();
8484

8585
let applicationFactory = engine.factoryFor(`controller:application`);
8686
let controllerFactory = applicationFactory || generateControllerFactory(engine, 'application');
8787
let controller: any;
8888
let self: RootReference<any>;
89-
let bucket: EngineState | EngineWithModelState;
90-
let tag: Tag;
91-
let modelRef = state.modelRef;
89+
let bucket: EngineState;
90+
let modelRef;
91+
92+
if (args.named.has(MODEL_ARG_NAME)) {
93+
modelRef = args.named.get(MODEL_ARG_NAME);
94+
}
95+
9296
if (modelRef === undefined) {
9397
controller = controllerFactory.create();
9498
self = new RootReference(controller);
95-
tag = CONSTANT_TAG;
96-
bucket = { engine, controller, self, tag };
99+
bucket = { engine, controller, self };
97100
} else {
98101
let model = modelRef.value();
99-
let modelRev = value(modelRef.tag);
100102
controller = controllerFactory.create({ model });
101103
self = new RootReference(controller);
102-
tag = modelRef.tag;
103-
bucket = { engine, controller, self, tag, modelRef, modelRev };
104+
bucket = { engine, controller, self, modelRef };
104105
}
105106

106107
return bucket;
@@ -110,8 +111,12 @@ class MountManager
110111
return self;
111112
}
112113

113-
getTag(state: EngineState | EngineWithModelState): Tag {
114-
return state.tag;
114+
getTag(state: EngineState): Tag {
115+
if (state.modelRef) {
116+
return state.modelRef.tag;
117+
} else {
118+
return CONSTANT_TAG;
119+
}
115120
}
116121

117122
getDestructor({ engine }: EngineState): Option<Destroyable> {
@@ -124,13 +129,9 @@ class MountManager
124129
}
125130
}
126131

127-
update(bucket: EngineWithModelState): void {
128-
let { controller, modelRef, modelRev } = bucket;
129-
if (!validate(modelRef.tag, modelRev!)) {
130-
let model = modelRef.value();
131-
bucket.modelRev = value(modelRef.tag);
132-
controller.set('model', model);
133-
}
132+
update({ controller, modelRef }: EngineState): void {
133+
assert('[BUG] `update` should only be called when modelRef is present', modelRef !== undefined);
134+
controller.set('model', modelRef!.value());
134135
}
135136
}
136137

@@ -140,7 +141,7 @@ export class MountDefinition implements ComponentDefinition {
140141
public state: EngineDefinitionState;
141142
public manager = MOUNT_MANAGER;
142143

143-
constructor(name: string, modelRef: VersionedPathReference<Opaque> | undefined) {
144-
this.state = { name, modelRef };
144+
constructor(name: string) {
145+
this.state = { name };
145146
}
146147
}

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

+59-21
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
*/
44
import { OwnedTemplateMeta } from '@ember/-internals/views';
55
import { assert } from '@ember/debug';
6-
import { Opaque, Option } from '@glimmer/interfaces';
6+
import { DEBUG } from '@glimmer/env';
7+
import { Option } from '@glimmer/interfaces';
78
import { OpcodeBuilder } from '@glimmer/opcode-compiler';
89
import { Tag, VersionedPathReference } from '@glimmer/reference';
910
import {
1011
Arguments,
12+
CapturedArguments,
1113
CurriedComponentDefinition,
1214
curry,
15+
EMPTY_ARGS,
1316
UNDEFINED_REFERENCE,
1417
VM,
1518
} from '@glimmer/runtime';
1619
import * as WireFormat from '@glimmer/wire-format';
17-
import { MountDefinition } from '../component-managers/mount';
20+
import { MODEL_ARG_NAME, MountDefinition } from '../component-managers/mount';
1821
import Environment from '../environment';
1922

2023
export function mountHelper(
@@ -23,8 +26,38 @@ export function mountHelper(
2326
): VersionedPathReference<CurriedComponentDefinition | null> {
2427
let env = vm.env as Environment;
2528
let nameRef = args.positional.at(0);
26-
let modelRef = args.named.has('model') ? args.named.get('model') : undefined;
27-
return new DynamicEngineReference(nameRef, env, modelRef);
29+
let captured: Option<CapturedArguments> = null;
30+
31+
// TODO: the functionailty to create a proper CapturedArgument should be
32+
// exported by glimmer, or that it should provide an overload for `curry`
33+
// that takes `PreparedArguments`
34+
if (args.named.has('model')) {
35+
assert('[BUG] this should already be checked by the macro', args.named.length === 1);
36+
37+
let named = args.named.capture();
38+
let { tag } = named;
39+
40+
// TODO delete me after EMBER_ROUTING_MODEL_ARG has shipped
41+
if (DEBUG && MODEL_ARG_NAME !== 'model') {
42+
assert('[BUG] named._map is not null', named['_map'] === null);
43+
named.names = [MODEL_ARG_NAME];
44+
}
45+
46+
captured = {
47+
tag,
48+
positional: EMPTY_ARGS.positional,
49+
named,
50+
length: 1,
51+
value() {
52+
return {
53+
named: this.named.value(),
54+
positional: this.positional.value(),
55+
};
56+
},
57+
};
58+
}
59+
60+
return new DynamicEngineReference(nameRef, env, captured);
2861
}
2962

3063
/**
@@ -78,33 +111,38 @@ export function mountMacro(
78111
params!.length === 1
79112
);
80113

114+
if (DEBUG && hash) {
115+
let keys = hash[0];
116+
let extra = keys.filter(k => k !== 'model');
117+
118+
assert(
119+
'You can only pass a `model` argument to the {{mount}} helper, ' +
120+
'e.g. {{mount "profile-engine" model=this.profile}}. ' +
121+
`You passed ${extra.join(',')}.`,
122+
extra.length === 0
123+
);
124+
}
125+
81126
let expr: WireFormat.Expressions.Helper = [WireFormat.Ops.Helper, '-mount', params || [], hash];
82127
builder.dynamicComponent(expr, null, [], null, false, null, null);
83128
return true;
84129
}
85130

86-
class DynamicEngineReference {
131+
class DynamicEngineReference implements VersionedPathReference<Option<CurriedComponentDefinition>> {
87132
public tag: Tag;
88-
public nameRef: VersionedPathReference<any | null | undefined>;
89-
public modelRef: VersionedPathReference<Opaque> | undefined;
90-
public env: Environment;
91-
private _lastName: string | null;
92-
private _lastDef: CurriedComponentDefinition | null;
133+
private _lastName: Option<string> = null;
134+
private _lastDef: Option<CurriedComponentDefinition> = null;
135+
93136
constructor(
94-
nameRef: VersionedPathReference<any | undefined | null>,
95-
env: Environment,
96-
modelRef: VersionedPathReference<Opaque> | undefined
137+
public nameRef: VersionedPathReference<any | undefined | null>,
138+
public env: Environment,
139+
public args: Option<CapturedArguments>
97140
) {
98141
this.tag = nameRef.tag;
99-
this.nameRef = nameRef;
100-
this.modelRef = modelRef;
101-
this.env = env;
102-
this._lastName = null;
103-
this._lastDef = null;
104142
}
105143

106-
value() {
107-
let { env, nameRef, modelRef } = this;
144+
value(): Option<CurriedComponentDefinition> {
145+
let { env, nameRef, args } = this;
108146
let name = nameRef.value();
109147

110148
if (typeof name === 'string') {
@@ -122,7 +160,7 @@ class DynamicEngineReference {
122160
}
123161

124162
this._lastName = name;
125-
this._lastDef = curry(new MountDefinition(name, modelRef));
163+
this._lastDef = curry(new MountDefinition(name), args);
126164

127165
return this._lastDef;
128166
} else {

packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js

+28-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { moduleFor, ApplicationTestCase, strip, runTaskNext } from 'internal-test-helpers';
22

3-
import Controller from '@ember/controller';
4-
import { RSVP } from '@ember/-internals/runtime';
53
import { Component } from '@ember/-internals/glimmer';
6-
import Engine from '@ember/engine';
74
import { Route } from '@ember/-internals/routing';
5+
import { RSVP } from '@ember/-internals/runtime';
6+
import { EMBER_ROUTING_MODEL_ARG } from '@ember/canary-features';
7+
import Controller from '@ember/controller';
8+
import Engine from '@ember/engine';
89
import { next } from '@ember/runloop';
910

1011
import { compile } from '../../utils/helpers';
@@ -517,7 +518,12 @@ moduleFor(
517518
},
518519
})
519520
);
520-
this.register('template:application_error', compile('Error! {{model.message}}'));
521+
this.register(
522+
'template:application_error',
523+
compile(
524+
EMBER_ROUTING_MODEL_ARG ? 'Error! {{@model.message}}' : 'Error! {{this.model.message}}'
525+
)
526+
);
521527
this.register(
522528
'route:post',
523529
Route.extend({
@@ -556,7 +562,12 @@ moduleFor(
556562
},
557563
})
558564
);
559-
this.register('template:error', compile('Error! {{model.message}}'));
565+
this.register(
566+
'template:error',
567+
compile(
568+
EMBER_ROUTING_MODEL_ARG ? 'Error! {{@model.message}}' : 'Error! {{this.model.message}}'
569+
)
570+
);
560571
this.register(
561572
'route:post',
562573
Route.extend({
@@ -595,7 +606,12 @@ moduleFor(
595606
},
596607
})
597608
);
598-
this.register('template:post_error', compile('Error! {{model.message}}'));
609+
this.register(
610+
'template:post_error',
611+
compile(
612+
EMBER_ROUTING_MODEL_ARG ? 'Error! {{@model.message}}' : 'Error! {{this.model.message}}'
613+
)
614+
);
599615
this.register(
600616
'route:post',
601617
Route.extend({
@@ -634,7 +650,12 @@ moduleFor(
634650
},
635651
})
636652
);
637-
this.register('template:post.error', compile('Error! {{model.message}}'));
653+
this.register(
654+
'template:post.error',
655+
compile(
656+
EMBER_ROUTING_MODEL_ARG ? 'Error! {{@model.message}}' : 'Error! {{this.model.message}}'
657+
)
658+
);
638659
this.register(
639660
'route:post.comments',
640661
Route.extend({

0 commit comments

Comments
 (0)