Skip to content

Commit d53b2e8

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 74b9c49 commit d53b2e8

File tree

5 files changed

+112
-10
lines changed

5 files changed

+112
-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: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import {
44
clientBuilder,
55
CurriedComponentDefinition,
66
curry,
7+
Cursor,
78
DynamicScope as GlimmerDynamicScope,
9+
ElementBuilder,
810
IteratorResult,
911
RenderResult,
1012
UNDEFINED_REFERENCE,
1113
} from '@glimmer/runtime';
14+
15+
import { serializeBuilder } from '@glimmer/node';
16+
1217
import { Opaque } from '@glimmer/util';
1318
import { assert } from 'ember-debug';
1419
import {
@@ -34,6 +39,7 @@ import { UnboundReference } from './utils/references';
3439
import OutletView from './views/outlet';
3540

3641
const { backburner } = run;
42+
export type IBuilder = (env: Environment, cursor: Cursor) => ElementBuilder;
3743

3844
export class DynamicScope implements GlimmerDynamicScope {
3945
constructor(
@@ -78,7 +84,9 @@ class RootState {
7884
template: OwnedTemplate,
7985
self: VersionedPathReference<Opaque>,
8086
parentElement: Simple.Element,
81-
dynamicScope: DynamicScope) {
87+
dynamicScope: DynamicScope,
88+
builder: IBuilder
89+
) {
8290
assert(`You cannot render \`${self.value()}\` without a template.`, template !== undefined);
8391

8492
this.id = getViewId(root);
@@ -96,7 +104,7 @@ class RootState {
96104
let iterator = template.renderLayout({
97105
self,
98106
env,
99-
builder: clientBuilder(env, { element: parentElement, nextSibling: null}),
107+
builder: builder(env, { element: parentElement, nextSibling: null}),
100108
dynamicScope
101109
});
102110
let iteratorResult: IteratorResult<RenderResult>;
@@ -246,8 +254,9 @@ export abstract class Renderer {
246254
private _lastRevision: number;
247255
private _isRenderingRoots: boolean;
248256
private _removedRoots: RootState[];
257+
private _builder: IBuilder;
249258

250-
constructor(env: Environment, rootTemplate: OwnedTemplate, _viewRegistry = fallbackViewRegistry, destinedForDOM = false) {
259+
constructor(env: Environment, rootTemplate: OwnedTemplate, _viewRegistry = fallbackViewRegistry, destinedForDOM = false, builder = clientBuilder) {
251260
this._env = env;
252261
this._rootTemplate = rootTemplate;
253262
this._viewRegistry = _viewRegistry;
@@ -257,6 +266,7 @@ export abstract class Renderer {
257266
this._lastRevision = -1;
258267
this._isRenderingRoots = false;
259268
this._removedRoots = [];
269+
this._builder = builder;
260270
}
261271

262272
// renderer HOOKS
@@ -277,7 +287,7 @@ export abstract class Renderer {
277287
target: Simple.Element) {
278288
let self = new UnboundReference(definition);
279289
let dynamicScope = new DynamicScope(null, UNDEFINED_REFERENCE);
280-
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);
281291
this._renderRoot(rootState);
282292
}
283293

@@ -486,8 +496,8 @@ export abstract class Renderer {
486496
}
487497

488498
export class InertRenderer extends Renderer {
489-
static create({ env, rootTemplate, _viewRegistry }: {env: Environment, rootTemplate: OwnedTemplate, _viewRegistry: any}) {
490-
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);
491501
}
492502

493503
getElement(_view: Opaque): Simple.Element | undefined {
@@ -496,8 +506,8 @@ export class InertRenderer extends Renderer {
496506
}
497507

498508
export class InteractiveRenderer extends Renderer {
499-
static create({ env, rootTemplate, _viewRegistry }: {env: Environment, rootTemplate: OwnedTemplate, _viewRegistry: any}) {
500-
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);
501511
}
502512

503513
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)