import { Container } from 'aurelia-dependency-injection'; import { createOverrideContext, OverrideContext } from 'aurelia-binding'; import { ViewSlot, ViewLocator, BehaviorInstruction, CompositionTransaction, CompositionEngine, ShadowDOM, SwapStrategies, ResourceDescription, HtmlBehaviorResource, CompositionTransactionNotifier, View, CompositionTransactionOwnershipToken, Controller, ViewFactory, CompositionContext, IStaticResourceConfig, IStaticViewConfig } from 'aurelia-templating'; import { Router } from 'aurelia-router'; import { Origin } from 'aurelia-metadata'; import { DOM } from 'aurelia-pal'; import { IRouterViewViewPortInstruction } from './interfaces'; class EmptyLayoutViewModel { } /** * Implementation of Aurelia Router ViewPort. Responsible for loading route, composing and swapping routes views */ export class RouterView { /**@internal */ static inject() { return [DOM.Element, Container, ViewSlot, Router, ViewLocator, CompositionTransaction, CompositionEngine]; } /** * @internal Actively avoid using decorator to reduce the amount of code generated * * There is no view to compose by default in a router view * This custom element is responsible for composing its own view, based on current config */ static $view: IStaticViewConfig = null; /** * @internal Actively avoid using decorator to reduce the amount of code generated */ static $resource: IStaticResourceConfig = { name: 'router-view', bindables: ['swapOrder', 'layoutView', 'layoutViewModel', 'layoutModel', 'inherit-binding-context'] as any }; /** * Swapping order when going to a new route. By default, supports 3 value: before, after, with * - before = new in -> old out * - after = old out -> new in * - with = new in + old out * * These values are defined by swapStrategies export in aurelia-templating/ aurelia-framework * Can be extended there and used here */ swapOrder?: 'before' | 'after' | 'with'; /** * Layout view used for this router-view layout, if no layout-viewmodel specified */ layoutView?: any; /** * Layout view model used as binding context for this router-view layout * Actual type would be {string | Constructable | object} */ layoutViewModel?: any; /** * Layout model used to activate layout view model, if specified with `layoutViewModel` */ layoutModel?: any; /** * Element associated with this custom element */ readonly element: Element; /** * Current router associated with this */ readonly router: Router; /** * Container at this level */ container: Container; /** * @internal * the view slot for adding / removing Routing related views created dynamically */ viewSlot: ViewSlot; /** * @internal * Used to mimic partially functionalities of CompositionEngine */ viewLocator: ViewLocator; /** * @internal * View composed by the CompositionEngine, depends on layout / viewports/ moduleId / viewModel of routeconfig */ view: View; /** * @internal * The view where this `` is placed in */ owningView: View; /** * @internal * Composition Transaction of initial composition transaction, when this is created */ compositionTransaction: CompositionTransaction; /** * @internal * CompositionEngine instance, responsible for composing view/view model during process changes phase of this */ compositionEngine: CompositionEngine; /** * Composition transaction notifier instance. Created when this router-view composing its instruction * for the first time. * Null on 2nd time and after. * @internal */ compositionTransactionNotifier: CompositionTransactionNotifier; /** * @internal */ compositionTransactionOwnershipToken: CompositionTransactionOwnershipToken; /** * @internal */ overrideContext: OverrideContext; constructor( element: Element, container: Container, viewSlot: ViewSlot, router: Router, viewLocator: ViewLocator, compositionTransaction: CompositionTransaction, compositionEngine: CompositionEngine ) { this.element = element; this.container = container; this.viewSlot = viewSlot; this.router = router; this.viewLocator = viewLocator; this.compositionTransaction = compositionTransaction; this.compositionEngine = compositionEngine; // add this to router view ports lookup based on name attribute // when this router is the root router-view // also trigger AppRouter registerViewPort extra flow this.router.registerViewPort(this, this.element.getAttribute('name')); // Each process its instruction as a composition transaction // there are differences between intial composition and subsequent compositions // also there are differences between root composition and child composition // mark the first composition transaction with a property initialComposition to distinguish it // when the root gets new instruction for the first time if (!('initialComposition' in compositionTransaction)) { compositionTransaction.initialComposition = true; this.compositionTransactionNotifier = compositionTransaction.enlist(); } } created(owningView: View): void { this.owningView = owningView; } bind(bindingContext: any, overrideContext: OverrideContext): void { // router needs to get access to view model of current route parent // doing it in generic way via viewModel property on container this.container.viewModel = bindingContext; this.overrideContext = overrideContext; } /** * Implementation of `aurelia-router` ViewPort interface, responsible for templating related part in routing Pipeline */ process($viewPortInstruction: any, waitToSwap?: boolean): Promise { // have strong typings without exposing it in public typings, this is to ensure maximum backward compat const viewPortInstruction = $viewPortInstruction as IRouterViewViewPortInstruction; const component = viewPortInstruction.component; const childContainer = component.childContainer; const viewModel = component.viewModel; const viewModelResource = component.viewModelResource as unknown as ResourceDescription; const metadata = viewModelResource.metadata; const config = component.router.currentInstruction.config; const viewPortConfig = config.viewPorts ? (config.viewPorts[viewPortInstruction.name] || {}) : {}; (childContainer.get(RouterViewLocator) as RouterViewLocator)._notify(this); // layoutInstruction is our layout viewModel const layoutInstruction = { viewModel: viewPortConfig.layoutViewModel || config.layoutViewModel || this.layoutViewModel, view: viewPortConfig.layoutView || config.layoutView || this.layoutView, model: viewPortConfig.layoutModel || config.layoutModel || this.layoutModel, router: viewPortInstruction.component.router, childContainer: childContainer, viewSlot: this.viewSlot }; // viewport will be a thin wrapper around composition engine // to process instruction/configuration from users // preparing all information related to a composition process // first by getting view strategy of a ViewPortComponent View const viewStrategy = this.viewLocator.getViewStrategy(component.view || viewModel); if (viewStrategy && component.view) { viewStrategy.makeRelativeTo(Origin.get(component.router.container.viewModel.constructor).moduleId); } // using metadata of a custom element view model to load appropriate view-factory instance return metadata .load(childContainer, viewModelResource.value, null, viewStrategy, true) // for custom element, viewFactory typing is always ViewFactory // for custom attribute, it will be HtmlBehaviorResource .then((viewFactory: ViewFactory | HtmlBehaviorResource) => { // if this is not the first time that this is composing its instruction // try to capture ownership of the composition transaction // child will not be able to capture, since root typically captures // the ownership token if (!this.compositionTransactionNotifier) { this.compositionTransactionOwnershipToken = this.compositionTransaction.tryCapture(); } if (layoutInstruction.viewModel || layoutInstruction.view) { viewPortInstruction.layoutInstruction = layoutInstruction; } const viewPortComponentBehaviorInstruction = BehaviorInstruction.dynamic( this.element, viewModel, viewFactory as ViewFactory ); viewPortInstruction.controller = metadata.create(childContainer, viewPortComponentBehaviorInstruction); if (waitToSwap) { return null; } this.swap(viewPortInstruction); }); } swap($viewPortInstruction: any): void | Promise { // have strong typings without exposing it in public typings, this is to ensure maximum backward compat const viewPortInstruction: IRouterViewViewPortInstruction = $viewPortInstruction; const viewPortController = viewPortInstruction.controller; const layoutInstruction = viewPortInstruction.layoutInstruction; const previousView = this.view; // Final step of swapping a ViewPortComponent const work = () => { const swapStrategy = SwapStrategies[this.swapOrder] || SwapStrategies.after; const viewSlot = this.viewSlot; swapStrategy( viewSlot, previousView, () => Promise.resolve(viewSlot.add(this.view)) ).then(() => { this._notify(); }); }; // Ensure all users setups have been completed const ready = (owningView_or_layoutView: View) => { viewPortController.automate(this.overrideContext, owningView_or_layoutView); const transactionOwnerShipToken = this.compositionTransactionOwnershipToken; // if this router-view is the root of a normal startup via aurelia.setRoot // attemp to take control of the transaction // if ownership can be taken // wait for transaction to complete before swapping if (transactionOwnerShipToken) { return transactionOwnerShipToken .waitForCompositionComplete() .then(() => { this.compositionTransactionOwnershipToken = null; return work(); }); } // otherwise, just swap return work(); }; // If there is layout instruction, new to compose layout before processing ViewPortComponent // layout controller/view/view-model is composed using composition engine APIs if (layoutInstruction) { if (!layoutInstruction.viewModel) { // createController chokes if there's no viewmodel, so create a dummy one // but avoid using a POJO as it creates unwanted metadata in Object constructor layoutInstruction.viewModel = new EmptyLayoutViewModel(); } // using composition engine to create compose layout return this.compositionEngine // first create controller from layoutInstruction // and treat it as CompositionContext // then emulate slot projection with ViewPortComponent view .createController(layoutInstruction as CompositionContext) .then((layoutController: Controller) => { const layoutView = layoutController.view; ShadowDOM.distributeView(viewPortController.view, layoutController.slots || layoutView.slots); // when there is a layout // view hierarchy is: owner view -> layout view -> ViewPortComponent view layoutController.automate(createOverrideContext(layoutInstruction.viewModel), this.owningView); layoutView.children.push(viewPortController.view); return layoutView || layoutController; }) .then((newView: View | Controller) => { this.view = newView as View; return ready(newView as View); }); } // if there is no layout, then get ViewPortComponent view ready as view property // and process controller/swapping // when there is no layout // view hierarchy is: owner view -> ViewPortComponent view this.view = viewPortController.view; return ready(this.owningView); } /** * Notify composition transaction that this router has finished processing * Happens when this is the root router-view * @internal */ _notify() { const notifier = this.compositionTransactionNotifier; if (notifier) { notifier.done(); this.compositionTransactionNotifier = null; } } } /** * Locator which finds the nearest RouterView, relative to the current dependency injection container. */ export class RouterViewLocator { /*@internal */ promise: Promise; /*@internal */ resolve: (val?: any) => void; /** * Creates an instance of the RouterViewLocator class. */ constructor() { this.promise = new Promise((resolve) => this.resolve = resolve); } /** * Finds the nearest RouterView instance. * @returns A promise that will be resolved with the located RouterView instance. */ findNearest(): Promise { return this.promise; } /**@internal */ _notify(routerView: RouterView): void { this.resolve(routerView); } }