UNPKG

14.1 kBPlain TextView Raw
1import { Container } from 'aurelia-dependency-injection';
2import { createOverrideContext, OverrideContext } from 'aurelia-binding';
3import {
4 ViewSlot,
5 ViewLocator,
6 BehaviorInstruction,
7 CompositionTransaction,
8 CompositionEngine,
9 ShadowDOM,
10 SwapStrategies,
11 ResourceDescription,
12 HtmlBehaviorResource,
13 CompositionTransactionNotifier,
14 View,
15 CompositionTransactionOwnershipToken,
16 Controller,
17 ViewFactory,
18 CompositionContext,
19 IStaticResourceConfig,
20 IStaticViewConfig
21} from 'aurelia-templating';
22import {
23 Router
24} from 'aurelia-router';
25import { Origin } from 'aurelia-metadata';
26import { DOM } from 'aurelia-pal';
27import { IRouterViewViewPortInstruction } from './interfaces';
28
29class EmptyLayoutViewModel {
30
31}
32
33/**
34 * Implementation of Aurelia Router ViewPort. Responsible for loading route, composing and swapping routes views
35 */
36export class RouterView {
37
38 /**@internal */
39 static inject() {
40 return [DOM.Element, Container, ViewSlot, Router, ViewLocator, CompositionTransaction, CompositionEngine];
41 }
42
43 /**
44 * @internal Actively avoid using decorator to reduce the amount of code generated
45 *
46 * There is no view to compose by default in a router view
47 * This custom element is responsible for composing its own view, based on current config
48 */
49 static $view: IStaticViewConfig = null;
50 /**
51 * @internal Actively avoid using decorator to reduce the amount of code generated
52 */
53 static $resource: IStaticResourceConfig = {
54 name: 'router-view',
55 bindables: ['swapOrder', 'layoutView', 'layoutViewModel', 'layoutModel', 'inherit-binding-context'] as any
56 };
57
58 /**
59 * Swapping order when going to a new route. By default, supports 3 value: before, after, with
60 * - before = new in -> old out
61 * - after = old out -> new in
62 * - with = new in + old out
63 *
64 * These values are defined by swapStrategies export in aurelia-templating/ aurelia-framework
65 * Can be extended there and used here
66 */
67 swapOrder?: 'before' | 'after' | 'with';
68
69 /**
70 * Layout view used for this router-view layout, if no layout-viewmodel specified
71 */
72 layoutView?: any;
73
74 /**
75 * Layout view model used as binding context for this router-view layout
76 * Actual type would be {string | Constructable | object}
77 */
78 layoutViewModel?: any;
79
80 /**
81 * Layout model used to activate layout view model, if specified with `layoutViewModel`
82 */
83 layoutModel?: any;
84
85 /**
86 * Element associated with this <router-view/> custom element
87 */
88 readonly element: Element;
89
90 /**
91 * Current router associated with this <router-view/>
92 */
93 readonly router: Router;
94
95 /**
96 * Container at this <router-view/> level
97 */
98 container: Container;
99
100 /**
101 * @internal
102 * the view slot for adding / removing Routing related views created dynamically
103 */
104 viewSlot: ViewSlot;
105
106 /**
107 * @internal
108 * Used to mimic partially functionalities of CompositionEngine
109 */
110 viewLocator: ViewLocator;
111
112 /**
113 * @internal
114 * View composed by the CompositionEngine, depends on layout / viewports/ moduleId / viewModel of routeconfig
115 */
116 view: View;
117
118 /**
119 * @internal
120 * The view where this `<router-view/>` is placed in
121 */
122 owningView: View;
123
124 /**
125 * @internal
126 * Composition Transaction of initial composition transaction, when this <router-view/> is created
127 */
128 compositionTransaction: CompositionTransaction;
129
130 /**
131 * @internal
132 * CompositionEngine instance, responsible for composing view/view model during process changes phase of this <router-view/>
133 */
134 compositionEngine: CompositionEngine;
135
136 /**
137 * Composition transaction notifier instance. Created when this router-view composing its instruction
138 * for the first time.
139 * Null on 2nd time and after.
140 * @internal
141 */
142 compositionTransactionNotifier: CompositionTransactionNotifier;
143
144 /**
145 * @internal
146 */
147 compositionTransactionOwnershipToken: CompositionTransactionOwnershipToken;
148
149 /**
150 * @internal
151 */
152 overrideContext: OverrideContext;
153
154 constructor(
155 element: Element,
156 container: Container,
157 viewSlot: ViewSlot,
158 router: Router,
159 viewLocator: ViewLocator,
160 compositionTransaction: CompositionTransaction,
161 compositionEngine: CompositionEngine
162 ) {
163 this.element = element;
164 this.container = container;
165 this.viewSlot = viewSlot;
166 this.router = router;
167 this.viewLocator = viewLocator;
168 this.compositionTransaction = compositionTransaction;
169 this.compositionEngine = compositionEngine;
170 // add this <router-view/> to router view ports lookup based on name attribute
171 // when this router is the root router-view
172 // also trigger AppRouter registerViewPort extra flow
173 this.router.registerViewPort(this, this.element.getAttribute('name'));
174
175 // Each <router-view/> process its instruction as a composition transaction
176 // there are differences between intial composition and subsequent compositions
177 // also there are differences between root composition and child <router-view/> composition
178 // mark the first composition transaction with a property initialComposition to distinguish it
179 // when the root <router-view/> gets new instruction for the first time
180 if (!('initialComposition' in compositionTransaction)) {
181 compositionTransaction.initialComposition = true;
182 this.compositionTransactionNotifier = compositionTransaction.enlist();
183 }
184 }
185
186 created(owningView: View): void {
187 this.owningView = owningView;
188 }
189
190 bind(bindingContext: any, overrideContext: OverrideContext): void {
191 // router needs to get access to view model of current route parent
192 // doing it in generic way via viewModel property on container
193 this.container.viewModel = bindingContext;
194 this.overrideContext = overrideContext;
195 }
196
197 /**
198 * Implementation of `aurelia-router` ViewPort interface, responsible for templating related part in routing Pipeline
199 */
200 process($viewPortInstruction: any, waitToSwap?: boolean): Promise<void> {
201 // have strong typings without exposing it in public typings, this is to ensure maximum backward compat
202 const viewPortInstruction = $viewPortInstruction as IRouterViewViewPortInstruction;
203 const component = viewPortInstruction.component;
204 const childContainer = component.childContainer;
205 const viewModel = component.viewModel;
206 const viewModelResource = component.viewModelResource as unknown as ResourceDescription;
207 const metadata = viewModelResource.metadata;
208 const config = component.router.currentInstruction.config;
209 const viewPortConfig = config.viewPorts ? (config.viewPorts[viewPortInstruction.name] || {}) : {};
210
211 (childContainer.get(RouterViewLocator) as RouterViewLocator)._notify(this);
212
213 // layoutInstruction is our layout viewModel
214 const layoutInstruction = {
215 viewModel: viewPortConfig.layoutViewModel || config.layoutViewModel || this.layoutViewModel,
216 view: viewPortConfig.layoutView || config.layoutView || this.layoutView,
217 model: viewPortConfig.layoutModel || config.layoutModel || this.layoutModel,
218 router: viewPortInstruction.component.router,
219 childContainer: childContainer,
220 viewSlot: this.viewSlot
221 };
222
223 // viewport will be a thin wrapper around composition engine
224 // to process instruction/configuration from users
225 // preparing all information related to a composition process
226 // first by getting view strategy of a ViewPortComponent View
227 const viewStrategy = this.viewLocator.getViewStrategy(component.view || viewModel);
228 if (viewStrategy && component.view) {
229 viewStrategy.makeRelativeTo(Origin.get(component.router.container.viewModel.constructor).moduleId);
230 }
231
232 // using metadata of a custom element view model to load appropriate view-factory instance
233 return metadata
234 .load(childContainer, viewModelResource.value, null, viewStrategy, true)
235 // for custom element, viewFactory typing is always ViewFactory
236 // for custom attribute, it will be HtmlBehaviorResource
237 .then((viewFactory: ViewFactory | HtmlBehaviorResource) => {
238 // if this is not the first time that this <router-view/> is composing its instruction
239 // try to capture ownership of the composition transaction
240 // child <router-view/> will not be able to capture, since root <router-view/> typically captures
241 // the ownership token
242 if (!this.compositionTransactionNotifier) {
243 this.compositionTransactionOwnershipToken = this.compositionTransaction.tryCapture();
244 }
245
246 if (layoutInstruction.viewModel || layoutInstruction.view) {
247 viewPortInstruction.layoutInstruction = layoutInstruction;
248 }
249
250 const viewPortComponentBehaviorInstruction = BehaviorInstruction.dynamic(
251 this.element,
252 viewModel,
253 viewFactory as ViewFactory
254 );
255 viewPortInstruction.controller = metadata.create(childContainer, viewPortComponentBehaviorInstruction);
256
257 if (waitToSwap) {
258 return null;
259 }
260
261 this.swap(viewPortInstruction);
262 });
263 }
264
265 swap($viewPortInstruction: any): void | Promise<void> {
266 // have strong typings without exposing it in public typings, this is to ensure maximum backward compat
267 const viewPortInstruction: IRouterViewViewPortInstruction = $viewPortInstruction;
268 const viewPortController = viewPortInstruction.controller;
269 const layoutInstruction = viewPortInstruction.layoutInstruction;
270 const previousView = this.view;
271
272 // Final step of swapping a <router-view/> ViewPortComponent
273 const work = () => {
274 const swapStrategy = SwapStrategies[this.swapOrder] || SwapStrategies.after;
275 const viewSlot = this.viewSlot;
276
277 swapStrategy(
278 viewSlot,
279 previousView,
280 () => Promise.resolve(viewSlot.add(this.view))
281 ).then(() => {
282 this._notify();
283 });
284 };
285
286 // Ensure all users setups have been completed
287 const ready = (owningView_or_layoutView: View) => {
288 viewPortController.automate(this.overrideContext, owningView_or_layoutView);
289 const transactionOwnerShipToken = this.compositionTransactionOwnershipToken;
290 // if this router-view is the root <router-view/> of a normal startup via aurelia.setRoot
291 // attemp to take control of the transaction
292
293 // if ownership can be taken
294 // wait for transaction to complete before swapping
295 if (transactionOwnerShipToken) {
296 return transactionOwnerShipToken
297 .waitForCompositionComplete()
298 .then(() => {
299 this.compositionTransactionOwnershipToken = null;
300 return work();
301 });
302 }
303
304 // otherwise, just swap
305 return work();
306 };
307
308 // If there is layout instruction, new to compose layout before processing ViewPortComponent
309 // layout controller/view/view-model is composed using composition engine APIs
310 if (layoutInstruction) {
311 if (!layoutInstruction.viewModel) {
312 // createController chokes if there's no viewmodel, so create a dummy one
313 // but avoid using a POJO as it creates unwanted metadata in Object constructor
314 layoutInstruction.viewModel = new EmptyLayoutViewModel();
315 }
316
317 // using composition engine to create compose layout
318 return this.compositionEngine
319 // first create controller from layoutInstruction
320 // and treat it as CompositionContext
321 // then emulate slot projection with ViewPortComponent view
322 .createController(layoutInstruction as CompositionContext)
323 .then((layoutController: Controller) => {
324 const layoutView = layoutController.view;
325 ShadowDOM.distributeView(viewPortController.view, layoutController.slots || layoutView.slots);
326 // when there is a layout
327 // view hierarchy is: <router-view/> owner view -> layout view -> ViewPortComponent view
328 layoutController.automate(createOverrideContext(layoutInstruction.viewModel), this.owningView);
329 layoutView.children.push(viewPortController.view);
330 return layoutView || layoutController;
331 })
332 .then((newView: View | Controller) => {
333 this.view = newView as View;
334 return ready(newView as View);
335 });
336 }
337
338 // if there is no layout, then get ViewPortComponent view ready as view property
339 // and process controller/swapping
340 // when there is no layout
341 // view hierarchy is: <router-view/> owner view -> ViewPortComponent view
342 this.view = viewPortController.view;
343
344 return ready(this.owningView);
345 }
346
347 /**
348 * Notify composition transaction that this router has finished processing
349 * Happens when this <router-view/> is the root router-view
350 * @internal
351 */
352 _notify() {
353 const notifier = this.compositionTransactionNotifier;
354 if (notifier) {
355 notifier.done();
356 this.compositionTransactionNotifier = null;
357 }
358 }
359}
360
361/**
362* Locator which finds the nearest RouterView, relative to the current dependency injection container.
363*/
364export class RouterViewLocator {
365
366 /*@internal */
367 promise: Promise<any>;
368
369 /*@internal */
370 resolve: (val?: any) => void;
371
372 /**
373 * Creates an instance of the RouterViewLocator class.
374 */
375 constructor() {
376 this.promise = new Promise((resolve) => this.resolve = resolve);
377 }
378
379 /**
380 * Finds the nearest RouterView instance.
381 * @returns A promise that will be resolved with the located RouterView instance.
382 */
383 findNearest(): Promise<RouterView> {
384 return this.promise;
385 }
386
387 /**@internal */
388 _notify(routerView: RouterView): void {
389 this.resolve(routerView);
390 }
391}