Skip to content

Commit f90773e

Browse files
committed
Allow visit to specify Rehydration/Serialization or default
ClientBuilder Expose as a configurable option the `renderMode` option. This option would be to specify the clientBuilder to be used when Glimmer does it's append run. The serialization mode, used by SSR applications, is used to specify additional markings necessary to get enough fidelity to accurately rehydrate the DOM. For example, it would provide additional comment nodes with codes to ensure text nodes are separated when they are rather than merged. The rehydration mode is specifically designed to read DOM that is produced by the serialization mode and accurately reproduce it. A great description of how Rehydration works can be found in this commit: glimmerjs/glimmer-vm@316805b This PR allows the appropriate ElementBuilder interface to be used via the `visit` API. Additional Work: - Update Fastboot to pass the appropriate `renderMode` flag such that it generates the serialization format DOM - Update ember-cli-fastboot instance-initializer to not do a double boot and instead configure it to use the rehydration `renderMode` - See more information here: https://github.com/ember-fastboot/ember-cli-fastboot/blob/master/addon/instance-initializers/clear-double-boot.js Open Questions: - Is the `renderMode` flag Public API - Should this be put behind a feature flag - @rwjblue noted that this technically would be a bug fix as it would remove the double render problem with SSR ember apps
1 parent 3588959 commit f90773e

File tree

5 files changed

+109
-10
lines changed

5 files changed

+109
-10
lines changed

packages/ember-application/lib/system/application-instance.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,14 @@ function BootOptions(options = {}) {
342342
*/
343343
this.isInteractive = environment.hasDOM; // This default is overridable below
344344

345+
/**
346+
@property renderMode
347+
@type string
348+
@default false
349+
@public
350+
*/
351+
this.renderMode = options.renderMode;
352+
345353
/**
346354
Run in a full browser environment.
347355
@@ -481,6 +489,7 @@ BootOptions.prototype.toEnvironment = function() {
481489
// For compatibility with existing code
482490
env.hasDOM = this.isBrowser;
483491
env.isInteractive = this.isInteractive;
492+
env.renderMode = this.renderMode;
484493
env.options = this;
485494
return env;
486495
};

packages/ember-application/tests/system/visit_test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import Engine from '../../system/engine';
1212
import { Route } from 'ember-routing';
1313
import { Component, helper } from 'ember-glimmer';
1414
import { compile } from 'ember-template-compiler';
15+
import { ENV } from 'ember-environment';
1516

1617
function expectAsyncError() {
1718
RSVP.off('error');
@@ -21,6 +22,7 @@ moduleFor('Application - visit()', class extends ApplicationTestCase {
2122

2223
teardown() {
2324
RSVP.on('error', onerrorDefault);
25+
ENV._APPLICATION_TEMPLATE_WRAPPER = false;
2426
super.teardown();
2527
}
2628

@@ -35,6 +37,62 @@ moduleFor('Application - visit()', class extends ApplicationTestCase {
3537
);
3638
}
3739

40+
[`@test does not add serialize-mode markers by default`](assert) {
41+
let templateContent = '<div class="foo">Hi, Mom!</div>';
42+
this.addTemplate('index', templateContent);
43+
let rootElement = document.createElement('div');
44+
45+
let bootOptions = {
46+
isBrowser: false,
47+
rootElement
48+
};
49+
50+
ENV._APPLICATION_TEMPLATE_WRAPPER = false;
51+
return this.visit('/', bootOptions).then(()=> {
52+
assert.equal(rootElement.innerHTML, templateContent, 'without serialize flag renders as expected');
53+
});
54+
}
55+
56+
[`@test renderMode: rehydrate`](assert) {
57+
58+
let initialHTML = `<!--%+block:0%--><!--%+block:1%--><!--%+block:2%--><!--%+block:3%--><!--%+block:4%--><!--%+block:5%--><!--%+block:6%--><div class=\"foo\">Hi, Mom!</div><!--%-block:6%--><!--%-block:5%--><!--%-block:4%--><!--%-block:3%--><!--%-block:2%--><!--%-block:1%--><!--%-block:0%-->`;
59+
60+
this.addTemplate('index', '<div class="foo">Hi, Mom!</div>');
61+
let rootElement = document.createElement('div');
62+
rootElement.innerHTML = initialHTML;
63+
64+
let bootOptions = {
65+
isBrowser: false,
66+
rootElement,
67+
renderMode: 'rehydrate'
68+
};
69+
70+
ENV._APPLICATION_TEMPLATE_WRAPPER = false;
71+
return this.visit('/', bootOptions).then(()=> {
72+
// The exact contents of this may change when the underlying
73+
// implementation changes in the glimmer vm
74+
assert.equal(rootElement.innerHTML, '<div class="foo">Hi, Mom!</div>', 'precond - without serialize flag renders as expected');
75+
});
76+
}
77+
78+
[`@test renderMode: serialize`](assert) {
79+
this.addTemplate('index', '<div class="foo">Hi, Mom!</div>');
80+
let rootElement = document.createElement('div');
81+
82+
let bootOptions = {
83+
isBrowser: false,
84+
rootElement,
85+
renderMode: 'serialize'
86+
};
87+
88+
ENV._APPLICATION_TEMPLATE_WRAPPER = false;
89+
return this.visit('/', bootOptions).then(()=> {
90+
// The exact contents of this may change when the underlying
91+
// implementation changes in the glimmer vm
92+
let expectedTemplate = `<!--%+block:0%--><!--%+block:1%--><!--%+block:2%--><!--%+block:3%--><!--%+block:4%--><!--%+block:5%--><!--%+block:6%--><div class=\"foo\">Hi, Mom!</div><!--%-block:6%--><!--%-block:5%--><!--%-block:4%--><!--%-block:3%--><!--%-block:2%--><!--%-block:1%--><!--%-block:0%-->`;
93+
assert.equal(rootElement.innerHTML, expectedTemplate, 'precond - without serialize flag renders as expected');
94+
});
95+
}
3896
// This tests whether the application is "autobooted" by registering an
3997
// instance initializer and asserting it never gets run. Since this is
4098
// inherently testing that async behavior *doesn't* happen, we set a

packages/ember-glimmer/lib/dom.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
///<reference path="./simple-dom.d.ts" />
2-
export { DOMChanges, DOMTreeConstruction } from '@glimmer/runtime';
3-
export { NodeDOMTreeConstruction } from '@glimmer/node';
2+
export {
3+
DOMChanges,
4+
DOMTreeConstruction,
5+
clientBuilder,
6+
rehydrationBuilder
7+
} from '@glimmer/runtime';
8+
export {
9+
NodeDOMTreeConstruction,
10+
serializeBuilder
11+
} from '@glimmer/node';

packages/ember-glimmer/lib/renderer.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
clientBuilder,
55
CurriedComponentDefinition,
66
curry,
7+
Cursor,
78
DynamicScope as GlimmerDynamicScope,
9+
ElementBuilder,
810
IteratorResult,
911
renderMain,
1012
RenderResult,
@@ -35,6 +37,7 @@ import { UnboundReference } from './utils/references';
3537
import OutletView from './views/outlet';
3638

3739
const { backburner } = run;
40+
export type IBuilder = (env: Environment, cursor: Cursor) => ElementBuilder;
3841

3942
export class DynamicScope implements GlimmerDynamicScope {
4043
constructor(
@@ -79,7 +82,9 @@ class RootState {
7982
template: OwnedTemplate,
8083
self: VersionedPathReference<Opaque>,
8184
parentElement: Simple.Element,
82-
dynamicScope: DynamicScope) {
85+
dynamicScope: DynamicScope,
86+
builder: IBuilder
87+
) {
8388
assert(`You cannot render \`${self.value()}\` without a template.`, template !== undefined);
8489

8590
this.id = getViewId(root);
@@ -100,7 +105,7 @@ class RootState {
100105
env,
101106
self,
102107
dynamicScope,
103-
clientBuilder(env, { element: parentElement, nextSibling: null}),
108+
builder: builder(env, { element: parentElement, nextSibling: null}),
104109
handle
105110
);
106111
let iteratorResult: IteratorResult<RenderResult>;
@@ -249,8 +254,9 @@ export abstract class Renderer {
249254
private _lastRevision: number;
250255
private _isRenderingRoots: boolean;
251256
private _removedRoots: RootState[];
257+
private _builder: IBuilder;
252258

253-
constructor(env: Environment, rootTemplate: OwnedTemplate, _viewRegistry = fallbackViewRegistry, destinedForDOM = false) {
259+
constructor(env: Environment, rootTemplate: OwnedTemplate, _viewRegistry = fallbackViewRegistry, destinedForDOM = false, builder = clientBuilder) {
254260
this._env = env;
255261
this._rootTemplate = rootTemplate;
256262
this._viewRegistry = _viewRegistry;
@@ -260,6 +266,7 @@ export abstract class Renderer {
260266
this._lastRevision = -1;
261267
this._isRenderingRoots = false;
262268
this._removedRoots = [];
269+
this._builder = builder;
263270
}
264271

265272
// renderer HOOKS
@@ -280,7 +287,7 @@ export abstract class Renderer {
280287
target: Simple.Element) {
281288
let self = new UnboundReference(definition);
282289
let dynamicScope = new DynamicScope(null, UNDEFINED_REFERENCE);
283-
let rootState = new RootState(root, this._env, this._rootTemplate, self, target, dynamicScope);
290+
let rootState = new RootState(root, this._env, this._rootTemplate, self, target, dynamicScope, this._builder);
284291
this._renderRoot(rootState);
285292
}
286293

@@ -489,8 +496,8 @@ export abstract class Renderer {
489496
}
490497

491498
export class InertRenderer extends Renderer {
492-
static create({ env, rootTemplate, _viewRegistry }: {env: Environment, rootTemplate: OwnedTemplate, _viewRegistry: any}) {
493-
return new this(env, rootTemplate, _viewRegistry, false);
499+
static create({ env, rootTemplate, _viewRegistry, builder }: {env: Environment, rootTemplate: OwnedTemplate, _viewRegistry: any, builder: any}) {
500+
return new this(env, rootTemplate, _viewRegistry, false, builder);
494501
}
495502

496503
getElement(_view: Opaque): Simple.Element | undefined {
@@ -499,8 +506,8 @@ export class InertRenderer extends Renderer {
499506
}
500507

501508
export class InteractiveRenderer extends Renderer {
502-
static create({ env, rootTemplate, _viewRegistry }: {env: Environment, rootTemplate: OwnedTemplate, _viewRegistry: any}) {
503-
return new this(env, rootTemplate, _viewRegistry, true);
509+
static create({ env, rootTemplate, _viewRegistry, builder}: {env: Environment, rootTemplate: OwnedTemplate, _viewRegistry: any, builder: any}) {
510+
return new this(env, rootTemplate, _viewRegistry, true, builder);
504511
}
505512

506513
getElement(view: Opaque): Simple.Element | undefined {

packages/ember-glimmer/lib/setup-registry.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import LinkToComponent from './components/link-to';
66
import TextArea from './components/text_area';
77
import TextField from './components/text_field';
88
import {
9+
clientBuilder,
910
DOMChanges,
1011
DOMTreeConstruction,
1112
NodeDOMTreeConstruction,
13+
rehydrationBuilder,
14+
serializeBuilder,
1215
} from './dom';
1316
import Environment from './environment';
1417
import loc from './helpers/loc';
@@ -29,6 +32,20 @@ export function setupApplicationRegistry(registry: Registry) {
2932
registry.injection('service:-glimmer-environment', 'appendOperations', 'service:-dom-tree-construction');
3033
registry.injection('renderer', 'env', 'service:-glimmer-environment');
3134

35+
registry.register('service:-dom-builder', {
36+
create({ bootOptions }: { bootOptions: { renderMode: string } }) {
37+
let { renderMode } = bootOptions;
38+
39+
switch(renderMode) {
40+
case 'serialize': return serializeBuilder;
41+
case 'rehydrate': return rehydrationBuilder;
42+
default: return clientBuilder;
43+
}
44+
}
45+
});
46+
registry.injection('service:-dom-builder', 'bootOptions', '-environment:main');
47+
registry.injection('renderer', 'builder', 'service:-dom-builder');
48+
3249
registry.register(P`template:-root`, RootTemplate);
3350
registry.injection('renderer', 'rootTemplate', P`template:-root`);
3451

0 commit comments

Comments
 (0)