-
-
Notifications
You must be signed in to change notification settings - Fork 252
/
setup-rendering-context.js
189 lines (160 loc) · 5.9 KB
/
setup-rendering-context.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import { guidFor } from '@ember/object/internals';
import { run, next } from '@ember/runloop';
import { Promise } from 'rsvp';
import Ember from 'ember';
import global from './global';
import { getContext } from './setup-context';
export const RENDERING_CLEANUP = Object.create(null);
export function render(template) {
let context = getContext();
if (!context || typeof context.render !== 'function') {
throw new Error('Cannot call `render` without having first called `setupRenderingContext`.');
}
return context.render(template);
}
export function clearRender() {
let context = getContext();
if (!context || typeof context.clearRender !== 'function') {
throw new Error(
'Cannot call `clearRender` without having first called `setupRenderingContext`.'
);
}
return context.clearRender();
}
/*
* Responsible for:
*
* - Creating a basic rendering setup (e.g. setting up the main outlet view)
* - Adding `this.render` to the provided context
* - Adding `this.clearRender` to the provided context
*/
export default function(context) {
let guid = guidFor(context);
let testElementContainer = document.getElementById('ember-testing-container');
let fixtureResetValue = testElementContainer.innerHTML;
RENDERING_CLEANUP[guid] = [
() => {
testElementContainer.innerHTML = fixtureResetValue;
},
];
let { owner } = context;
let dispatcher = owner.lookup('event_dispatcher:main') || Ember.EventDispatcher.create();
dispatcher.setup({}, '#ember-testing');
let OutletView = owner.factoryFor
? owner.factoryFor('view:-outlet')
: owner._lookupFactory('view:-outlet');
let OutletTemplate = owner.lookup('template:-outlet');
let toplevelView = OutletView.create();
RENDERING_CLEANUP[guid].push(() => toplevelView.destroy());
let hasOutletTemplate = Boolean(OutletTemplate);
let outletState = {
render: {
owner,
into: undefined,
outlet: 'main',
name: 'application',
controller: context,
ViewClass: undefined,
template: OutletTemplate,
},
outlets: {},
};
let element, hasRendered;
let templateId = 0;
if (hasOutletTemplate) {
run(() => {
toplevelView.setOutletState(outletState);
});
}
context.render = function render(template) {
if (!template) {
throw new Error('you must pass a template to `render()`');
}
// ensure context.element is reset until after rendering has completed
element = undefined;
return new Promise(function asyncRender(resolve) {
// manually enter async land, so that rendering itself is always async (even though
// Ember <= 2.18 do not require async rendering)
next(function asyncRenderSetup() {
templateId += 1;
let templateFullName = `template:-undertest-${templateId}`;
owner.register(templateFullName, template);
let stateToRender = {
owner,
into: undefined,
outlet: 'main',
name: 'index',
controller: context,
ViewClass: undefined,
template: owner.lookup(templateFullName),
outlets: {},
};
if (hasOutletTemplate) {
stateToRender.name = 'index';
outletState.outlets.main = { render: stateToRender, outlets: {} };
} else {
stateToRender.name = 'application';
outletState = { render: stateToRender, outlets: {} };
}
toplevelView.setOutletState(outletState);
if (!hasRendered) {
// TODO: make this id configurable
run(toplevelView, 'appendTo', '#ember-testing');
hasRendered = true;
}
// using next here because the actual rendering does not happen until
// the renderer detects it is dirty (which happens on backburner's end
// hook), see the following implementation details:
//
// * [view:outlet](https://github.com/emberjs/ember.js/blob/f94a4b6aef5b41b96ef2e481f35e07608df01440/packages/ember-glimmer/lib/views/outlet.js#L129-L145) manually dirties its own tag upon `setOutletState`
// * [backburner's custom end hook](https://github.com/emberjs/ember.js/blob/f94a4b6aef5b41b96ef2e481f35e07608df01440/packages/ember-glimmer/lib/renderer.js#L145-L159) detects that the current revision of the root is no longer the latest, and triggers a new rendering transaction
next(function asyncUpdateElementAfterRender() {
// ensure the element is based on the wrapping toplevel view
// Ember still wraps the main application template with a
// normal tagged view
//
// In older Ember versions (2.4) the element itself is not stable,
// and therefore we cannot update the `this.element` until after the
// rendering is completed
element = document.querySelector('#ember-testing > .ember-view');
resolve();
});
});
});
};
Object.defineProperty(context, 'element', {
enumerable: true,
configurable: true,
get() {
return element;
},
});
if (global.jQuery) {
context.$ = function $(selector) {
// emulates Ember internal behavor of `this.$` in a component
// https://github.com/emberjs/ember.js/blob/v2.5.1/packages/ember-views/lib/views/states/has_element.js#L18
return selector ? global.jQuery(selector, element) : global.jQuery(element);
};
}
context.clearRender = function clearRender() {
return new Promise(function async_clearRender(resolve) {
element = undefined;
next(function async_clearRender() {
toplevelView.setOutletState({
render: {
owner,
into: undefined,
outlet: 'main',
name: 'application',
controller: context,
ViewClass: undefined,
template: undefined,
},
outlets: {},
});
// RE: next usage, see detailed comment above
next(resolve);
});
});
};
}