UNPKG

8.36 kBPlain TextView Raw
1import { Container } from 'aurelia-dependency-injection';
2import { DOM } from 'aurelia-pal';
3import { TaskQueue } from 'aurelia-task-queue';
4import { bindable, CompositionContext, CompositionEngine, customElement, noView, View, ViewResources, ViewSlot } from 'aurelia-templating';
5
6/**
7 * Available activation strategies for the view and view-model bound to `<compose/>` element
8 *
9 * @export
10 * @enum {string}
11 */
12export enum ActivationStrategy {
13 /**
14 * Default activation strategy; the 'activate' lifecycle hook will be invoked when the model changes.
15 */
16 InvokeLifecycle = 'invoke-lifecycle',
17 /**
18 * The view/view-model will be recreated, when the "model" changes.
19 */
20 Replace = 'replace'
21}
22
23/**
24 * Used to compose a new view / view-model template or bind to an existing instance.
25 */
26@noView
27@customElement('compose')
28export class Compose {
29
30 /**@internal */
31 static inject() {
32 return [DOM.Element, Container, CompositionEngine, ViewSlot, ViewResources, TaskQueue];
33 }
34
35 /**
36 * Model to bind the custom element to.
37 *
38 * @property model
39 * @type {CustomElement}
40 */
41 @bindable model: any;
42 /**
43 * View to bind the custom element to.
44 *
45 * @property view
46 * @type {HtmlElement}
47 */
48 @bindable view: any;
49 /**
50 * View-model to bind the custom element's template to.
51 *
52 * @property viewModel
53 * @type {Class}
54 */
55 @bindable viewModel: any;
56
57 /**
58 * Strategy to activate the view-model. Default is "invoke-lifecycle".
59 * Bind "replace" to recreate the view/view-model when the model changes.
60 *
61 * @property activationStrategy
62 * @type {ActivationStrategy}
63 */
64 @bindable activationStrategy: ActivationStrategy = ActivationStrategy.InvokeLifecycle;
65
66 /**
67 * SwapOrder to control the swapping order of the custom element's view.
68 *
69 * @property view
70 * @type {String}
71 */
72 @bindable swapOrder: any;
73
74 /**
75 *@internal
76 */
77 element: any;
78 /**
79 *@internal
80 */
81 container: any;
82 /**
83 *@internal
84 */
85 compositionEngine: any;
86 /**
87 *@internal
88 */
89 viewSlot: any;
90 /**
91 *@internal
92 */
93 viewResources: any;
94 /**
95 *@internal
96 */
97 taskQueue: any;
98 /**
99 *@internal
100 */
101 currentController: any;
102 /**
103 *@internal
104 */
105 currentViewModel: any;
106 /**
107 *@internal
108 */
109 changes: any;
110 /**
111 *@internal
112 */
113 owningView: View;
114 /**
115 *@internal
116 */
117 bindingContext: any;
118 /**
119 *@internal
120 */
121 overrideContext: any;
122 /**
123 *@internal
124 */
125 pendingTask: any;
126 /**
127 *@internal
128 */
129 updateRequested: any;
130
131 /**
132 * Creates an instance of Compose.
133 * @param element The Compose element.
134 * @param container The dependency injection container instance.
135 * @param compositionEngine CompositionEngine instance to compose the element.
136 * @param viewSlot The slot the view is injected in to.
137 * @param viewResources Collection of resources used to compile the the view.
138 * @param taskQueue The TaskQueue instance.
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 * Invoked when the component has been created.
154 *
155 * @param owningView The view that this component was created inside of.
156 */
157 created(owningView: View) {
158 this.owningView = owningView;
159 }
160
161 /**
162 * Used to set the bindingContext.
163 *
164 * @param bindingContext The context in which the view model is executed in.
165 * @param overrideContext The context in which the view model is executed in.
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 * Unbinds the Compose.
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 * Invoked everytime the bound model changes.
193 * @param newValue The new value.
194 * @param oldValue The old value.
195 */
196 // eslint-disable-next-line @typescript-eslint/no-unused-vars
197 modelChanged(newValue, oldValue) {
198 this.changes.model = newValue;
199 requestUpdate(this);
200 }
201
202 /**
203 * Invoked everytime the bound view changes.
204 * @param newValue The new value.
205 * @param oldValue The old value.
206 */
207 // eslint-disable-next-line @typescript-eslint/no-unused-vars
208 viewChanged(newValue, oldValue) {
209 this.changes.view = newValue;
210 requestUpdate(this);
211 }
212
213 /**
214 * Invoked everytime the bound view model changes.
215 * @param newValue The new value.
216 * @param oldValue The old value.
217 */
218 // eslint-disable-next-line @typescript-eslint/no-unused-vars
219 viewModelChanged(newValue, oldValue) {
220 this.changes.viewModel = newValue;
221 requestUpdate(this);
222 }
223}
224
225function isEmpty(obj) {
226 for (const _ in obj) {
227 return false;
228 }
229 return true;
230}
231
232function tryActivateViewModel(vm, model) {
233 if (vm && typeof vm.activate === 'function') {
234 return Promise.resolve(vm.activate(model));
235 }
236}
237
238function 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
252function processChanges(composer: Compose) {
253 const changes = composer.changes;
254 composer.changes = Object.create(null);
255
256 if (needsReInitialization(composer, changes)) {
257 // init context
258 let instruction = {
259 view: composer.view,
260 viewModel: composer.currentViewModel || composer.viewModel,
261 model: composer.model
262 } as CompositionContext;
263
264 // apply changes
265 instruction = Object.assign(instruction, changes);
266
267 // create context
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 // just try to activate the current view model
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
289function completeCompositionTask(composer) {
290 composer.pendingTask = null;
291 if (!isEmpty(composer.changes)) {
292 processChanges(composer);
293 }
294}
295
296function 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
305function 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}