/
composition-engine.js
227 lines (197 loc) · 7.67 KB
/
composition-engine.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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import {ViewLocator} from './view-locator';
import {ViewEngine} from './view-engine';
import {HtmlBehaviorResource} from './html-behavior';
import {BehaviorInstruction, ViewCompileInstruction} from './instructions';
import {CompositionTransaction} from './composition-transaction';
import {DOM} from 'aurelia-pal';
import {Container, inject} from 'aurelia-dependency-injection';
import {metadata} from 'aurelia-metadata';
/**
* Instructs the composition engine how to dynamically compose a component.
*/
interface CompositionContext {
/**
* The parent Container for the component creation.
*/
container: Container;
/**
* The child Container for the component creation. One will be created from the parent if not provided.
*/
childContainer?: Container;
/**
* The context in which the view model is executed in.
*/
bindingContext: any;
/**
* A secondary binding context that can override the standard context.
*/
overrideContext?: any;
/**
* The view model url or instance for the component.
*/
viewModel?: any;
/**
* Data to be passed to the "activate" hook on the view model.
*/
model?: any;
/**
* The HtmlBehaviorResource for the component.
*/
viewModelResource?: HtmlBehaviorResource;
/**
* The view resources for the view in which the component should be created.
*/
viewResources: ViewResources;
/**
* The view inside which this composition is happening.
*/
owningView?: View;
/**
* The view url or view strategy to override the default view location convention.
*/
view?: string | ViewStrategy;
/**
* The slot to push the dynamically composed component into.
*/
viewSlot: ViewSlot;
/**
* Should the composition system skip calling the "activate" hook on the view model.
*/
skipActivation?: boolean;
}
function tryActivateViewModel(context) {
if (context.skipActivation || typeof context.viewModel.activate !== 'function') {
return Promise.resolve();
}
return context.viewModel.activate(context.model) || Promise.resolve();
}
/**
* Used to dynamically compose components.
*/
@inject(ViewEngine, ViewLocator)
export class CompositionEngine {
/**
* Creates an instance of the CompositionEngine.
* @param viewEngine The ViewEngine used during composition.
*/
constructor(viewEngine: ViewEngine, viewLocator: ViewLocator) {
this.viewEngine = viewEngine;
this.viewLocator = viewLocator;
}
_createControllerAndSwap(context) {
function swap(controller) {
return Promise.resolve(context.viewSlot.removeAll(true)).then(() => {
if (context.currentController) {
context.currentController.unbind();
}
context.viewSlot.add(controller.view);
if (context.compositionTransactionNotifier) {
context.compositionTransactionNotifier.done();
}
return controller;
});
}
return this.createController(context).then(controller => {
controller.automate(context.overrideContext, context.owningView);
if (context.compositionTransactionOwnershipToken) {
return context.compositionTransactionOwnershipToken.waitForCompositionComplete().then(() => swap(controller));
} else {
return swap(controller);
}
});
}
/**
* Creates a controller instance for the component described in the context.
* @param context The CompositionContext that describes the component.
* @return A Promise for the Controller.
*/
createController(context: CompositionContext): Promise<Controller> {
let childContainer;
let viewModel;
let viewModelResource;
let m;
return this.ensureViewModel(context).then(tryActivateViewModel).then(() => {
childContainer = context.childContainer;
viewModel = context.viewModel;
viewModelResource = context.viewModelResource;
m = viewModelResource.metadata;
let viewStrategy = this.viewLocator.getViewStrategy(context.view || viewModel);
if (context.viewResources) {
viewStrategy.makeRelativeTo(context.viewResources.viewUrl);
}
return m.load(childContainer, viewModelResource.value, null, viewStrategy, true);
}).then(viewFactory => m.create(childContainer, BehaviorInstruction.dynamic(context.host, viewModel, viewFactory)));
}
/**
* Ensures that the view model and its resource are loaded for this context.
* @param context The CompositionContext to load the view model and its resource for.
* @return A Promise for the context.
*/
ensureViewModel(context: CompositionContext): Promise<CompositionContext> {
let childContainer = context.childContainer = (context.childContainer || context.container.createChild());
if (typeof context.viewModel === 'string') {
context.viewModel = context.viewResources
? context.viewResources.relativeToView(context.viewModel)
: context.viewModel;
return this.viewEngine.importViewModelResource(context.viewModel).then(viewModelResource => {
childContainer.autoRegister(viewModelResource.value);
if (context.host) {
childContainer.registerInstance(DOM.Element, context.host);
}
context.viewModel = childContainer.viewModel = childContainer.get(viewModelResource.value);
context.viewModelResource = viewModelResource;
return context;
});
}
let m = metadata.getOrCreateOwn(metadata.resource, HtmlBehaviorResource, context.viewModel.constructor);
m.elementName = m.elementName || 'dynamic-element';
m.initialize(context.container || childContainer, context.viewModel.constructor);
context.viewModelResource = { metadata: m, value: context.viewModel.constructor };
childContainer.viewModel = context.viewModel;
return Promise.resolve(context);
}
/**
* Dynamically composes a component.
* @param context The CompositionContext providing information on how the composition should occur.
* @return A Promise for the View or the Controller that results from the dynamic composition.
*/
compose(context: CompositionContext): Promise<View | Controller> {
context.childContainer = context.childContainer || context.container.createChild();
context.view = this.viewLocator.getViewStrategy(context.view);
let transaction = context.childContainer.get(CompositionTransaction);
let compositionTransactionOwnershipToken = transaction.tryCapture();
if (compositionTransactionOwnershipToken) {
context.compositionTransactionOwnershipToken = compositionTransactionOwnershipToken;
} else {
context.compositionTransactionNotifier = transaction.enlist();
}
if (context.viewModel) {
return this._createControllerAndSwap(context);
} else if (context.view) {
if (context.viewResources) {
context.view.makeRelativeTo(context.viewResources.viewUrl);
}
return context.view.loadViewFactory(this.viewEngine, new ViewCompileInstruction()).then(viewFactory => {
let result = viewFactory.create(context.childContainer);
result.bind(context.bindingContext, context.overrideContext);
let work = () => {
return Promise.resolve(context.viewSlot.removeAll(true)).then(() => {
context.viewSlot.add(result);
if (context.compositionTransactionNotifier) {
context.compositionTransactionNotifier.done();
}
return result;
});
};
if (context.compositionTransactionOwnershipToken) {
return context.compositionTransactionOwnershipToken.waitForCompositionComplete().then(work);
} else {
return work();
}
});
} else if (context.viewSlot) {
context.viewSlot.removeAll();
return Promise.resolve(null);
}
}
}