1 | import { Container } from 'aurelia-dependency-injection';
|
2 | import { DOM } from 'aurelia-pal';
|
3 | import { TaskQueue } from 'aurelia-task-queue';
|
4 | import { bindable, CompositionContext, CompositionEngine, customElement, noView, View, ViewResources, ViewSlot } from 'aurelia-templating';
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | export enum ActivationStrategy {
|
13 | |
14 |
|
15 |
|
16 | InvokeLifecycle = 'invoke-lifecycle',
|
17 | |
18 |
|
19 |
|
20 | Replace = 'replace'
|
21 | }
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | @noView
|
27 | @customElement('compose')
|
28 | export class Compose {
|
29 |
|
30 |
|
31 | static inject() {
|
32 | return [DOM.Element, Container, CompositionEngine, ViewSlot, ViewResources, TaskQueue];
|
33 | }
|
34 |
|
35 | |
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | @bindable model: any;
|
42 | |
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | @bindable view: any;
|
49 | |
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 | @bindable viewModel: any;
|
56 |
|
57 | |
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 | @bindable activationStrategy: ActivationStrategy = ActivationStrategy.InvokeLifecycle;
|
65 |
|
66 | |
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | @bindable swapOrder: any;
|
73 |
|
74 | |
75 |
|
76 |
|
77 | element: any;
|
78 | |
79 |
|
80 |
|
81 | container: any;
|
82 | |
83 |
|
84 |
|
85 | compositionEngine: any;
|
86 | |
87 |
|
88 |
|
89 | viewSlot: any;
|
90 | |
91 |
|
92 |
|
93 | viewResources: any;
|
94 | |
95 |
|
96 |
|
97 | taskQueue: any;
|
98 | |
99 |
|
100 |
|
101 | currentController: any;
|
102 | |
103 |
|
104 |
|
105 | currentViewModel: any;
|
106 | |
107 |
|
108 |
|
109 | changes: any;
|
110 | |
111 |
|
112 |
|
113 | owningView: View;
|
114 | |
115 |
|
116 |
|
117 | bindingContext: any;
|
118 | |
119 |
|
120 |
|
121 | overrideContext: any;
|
122 | |
123 |
|
124 |
|
125 | pendingTask: any;
|
126 | |
127 |
|
128 |
|
129 | updateRequested: any;
|
130 |
|
131 | |
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 | constructor(element, container, compositionEngine, viewSlot, viewResources, taskQueue) {
|
141 | this.element = element;
|
142 | this.container = container;
|
143 | this.compositionEngine = compositionEngine;
|
144 | this.viewSlot = viewSlot;
|
145 | this.viewResources = viewResources;
|
146 | this.taskQueue = taskQueue;
|
147 | this.currentController = null;
|
148 | this.currentViewModel = null;
|
149 | this.changes = Object.create(null);
|
150 | }
|
151 |
|
152 | |
153 |
|
154 |
|
155 |
|
156 |
|
157 | created(owningView: View) {
|
158 | this.owningView = owningView;
|
159 | }
|
160 |
|
161 | |
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | bind(bindingContext, overrideContext) {
|
168 | this.bindingContext = bindingContext;
|
169 | this.overrideContext = overrideContext;
|
170 | let changes = this.changes;
|
171 | changes.view = this.view;
|
172 | changes.viewModel = this.viewModel;
|
173 | changes.model = this.model;
|
174 | if (!this.pendingTask) {
|
175 | processChanges(this);
|
176 | }
|
177 | }
|
178 |
|
179 | |
180 |
|
181 |
|
182 | unbind() {
|
183 | this.changes = Object.create(null);
|
184 | this.bindingContext = null;
|
185 | this.overrideContext = null;
|
186 | let returnToCache = true;
|
187 | let skipAnimation = true;
|
188 | this.viewSlot.removeAll(returnToCache, skipAnimation);
|
189 | }
|
190 |
|
191 | |
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 | modelChanged(newValue, oldValue) {
|
198 | this.changes.model = newValue;
|
199 | requestUpdate(this);
|
200 | }
|
201 |
|
202 | |
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 | viewChanged(newValue, oldValue) {
|
209 | this.changes.view = newValue;
|
210 | requestUpdate(this);
|
211 | }
|
212 |
|
213 | |
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 | viewModelChanged(newValue, oldValue) {
|
220 | this.changes.viewModel = newValue;
|
221 | requestUpdate(this);
|
222 | }
|
223 | }
|
224 |
|
225 | function isEmpty(obj) {
|
226 | for (const _ in obj) {
|
227 | return false;
|
228 | }
|
229 | return true;
|
230 | }
|
231 |
|
232 | function tryActivateViewModel(vm, model) {
|
233 | if (vm && typeof vm.activate === 'function') {
|
234 | return Promise.resolve(vm.activate(model));
|
235 | }
|
236 | }
|
237 |
|
238 | function createInstruction(composer: Compose, instruction: CompositionContext): CompositionContext {
|
239 | return Object.assign(instruction, {
|
240 | bindingContext: composer.bindingContext,
|
241 | overrideContext: composer.overrideContext,
|
242 | owningView: composer.owningView,
|
243 | container: composer.container,
|
244 | viewSlot: composer.viewSlot,
|
245 | viewResources: composer.viewResources,
|
246 | currentController: composer.currentController,
|
247 | host: composer.element,
|
248 | swapOrder: composer.swapOrder
|
249 | });
|
250 | }
|
251 |
|
252 | function processChanges(composer: Compose) {
|
253 | const changes = composer.changes;
|
254 | composer.changes = Object.create(null);
|
255 |
|
256 | if (needsReInitialization(composer, changes)) {
|
257 |
|
258 | let instruction = {
|
259 | view: composer.view,
|
260 | viewModel: composer.currentViewModel || composer.viewModel,
|
261 | model: composer.model
|
262 | } as CompositionContext;
|
263 |
|
264 |
|
265 | instruction = Object.assign(instruction, changes);
|
266 |
|
267 |
|
268 | instruction = createInstruction(composer, instruction);
|
269 |
|
270 | composer.pendingTask = composer.compositionEngine.compose(instruction).then(controller => {
|
271 | composer.currentController = controller;
|
272 | composer.currentViewModel = controller ? controller.viewModel : null;
|
273 | });
|
274 | } else {
|
275 |
|
276 | composer.pendingTask = tryActivateViewModel(composer.currentViewModel, changes.model);
|
277 | if (!composer.pendingTask) { return; }
|
278 | }
|
279 |
|
280 | composer.pendingTask = composer.pendingTask
|
281 | .then(() => {
|
282 | completeCompositionTask(composer);
|
283 | }, reason => {
|
284 | completeCompositionTask(composer);
|
285 | throw reason;
|
286 | });
|
287 | }
|
288 |
|
289 | function completeCompositionTask(composer) {
|
290 | composer.pendingTask = null;
|
291 | if (!isEmpty(composer.changes)) {
|
292 | processChanges(composer);
|
293 | }
|
294 | }
|
295 |
|
296 | function requestUpdate(composer: Compose) {
|
297 | if (composer.pendingTask || composer.updateRequested) { return; }
|
298 | composer.updateRequested = true;
|
299 | composer.taskQueue.queueMicroTask(() => {
|
300 | composer.updateRequested = false;
|
301 | processChanges(composer);
|
302 | });
|
303 | }
|
304 |
|
305 | function needsReInitialization(composer: Compose, changes: any) {
|
306 | let activationStrategy = composer.activationStrategy;
|
307 | const vm = composer.currentViewModel;
|
308 | if (vm && typeof vm.determineActivationStrategy === 'function') {
|
309 | activationStrategy = vm.determineActivationStrategy();
|
310 | }
|
311 |
|
312 | return 'view' in changes
|
313 | || 'viewModel' in changes
|
314 | || activationStrategy === ActivationStrategy.Replace;
|
315 | }
|