import { Container } from 'aurelia-dependency-injection'; import { DOM } from 'aurelia-pal'; import { TaskQueue } from 'aurelia-task-queue'; import { bindable, CompositionContext, CompositionEngine, customElement, noView, View, ViewResources, ViewSlot } from 'aurelia-templating'; /** * Available activation strategies for the view and view-model bound to `` element * * @export * @enum {string} */ export enum ActivationStrategy { /** * Default activation strategy; the 'activate' lifecycle hook will be invoked when the model changes. */ InvokeLifecycle = 'invoke-lifecycle', /** * The view/view-model will be recreated, when the "model" changes. */ Replace = 'replace' } /** * Used to compose a new view / view-model template or bind to an existing instance. */ @noView @customElement('compose') export class Compose { /**@internal */ static inject() { return [DOM.Element, Container, CompositionEngine, ViewSlot, ViewResources, TaskQueue]; } /** * Model to bind the custom element to. * * @property model * @type {CustomElement} */ @bindable model: any; /** * View to bind the custom element to. * * @property view * @type {HtmlElement} */ @bindable view: any; /** * View-model to bind the custom element's template to. * * @property viewModel * @type {Class} */ @bindable viewModel: any; /** * Strategy to activate the view-model. Default is "invoke-lifecycle". * Bind "replace" to recreate the view/view-model when the model changes. * * @property activationStrategy * @type {ActivationStrategy} */ @bindable activationStrategy: ActivationStrategy = ActivationStrategy.InvokeLifecycle; /** * SwapOrder to control the swapping order of the custom element's view. * * @property view * @type {String} */ @bindable swapOrder: any; /** *@internal */ element: any; /** *@internal */ container: any; /** *@internal */ compositionEngine: any; /** *@internal */ viewSlot: any; /** *@internal */ viewResources: any; /** *@internal */ taskQueue: any; /** *@internal */ currentController: any; /** *@internal */ currentViewModel: any; /** *@internal */ changes: any; /** *@internal */ owningView: View; /** *@internal */ bindingContext: any; /** *@internal */ overrideContext: any; /** *@internal */ pendingTask: any; /** *@internal */ updateRequested: any; /** * Creates an instance of Compose. * @param element The Compose element. * @param container The dependency injection container instance. * @param compositionEngine CompositionEngine instance to compose the element. * @param viewSlot The slot the view is injected in to. * @param viewResources Collection of resources used to compile the the view. * @param taskQueue The TaskQueue instance. */ constructor(element, container, compositionEngine, viewSlot, viewResources, taskQueue) { this.element = element; this.container = container; this.compositionEngine = compositionEngine; this.viewSlot = viewSlot; this.viewResources = viewResources; this.taskQueue = taskQueue; this.currentController = null; this.currentViewModel = null; this.changes = Object.create(null); } /** * Invoked when the component has been created. * * @param owningView The view that this component was created inside of. */ created(owningView: View) { this.owningView = owningView; } /** * Used to set the bindingContext. * * @param bindingContext The context in which the view model is executed in. * @param overrideContext The context in which the view model is executed in. */ bind(bindingContext, overrideContext) { this.bindingContext = bindingContext; this.overrideContext = overrideContext; let changes = this.changes; changes.view = this.view; changes.viewModel = this.viewModel; changes.model = this.model; if (!this.pendingTask) { processChanges(this); } } /** * Unbinds the Compose. */ unbind() { this.changes = Object.create(null); this.bindingContext = null; this.overrideContext = null; let returnToCache = true; let skipAnimation = true; this.viewSlot.removeAll(returnToCache, skipAnimation); } /** * Invoked everytime the bound model changes. * @param newValue The new value. * @param oldValue The old value. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars modelChanged(newValue, oldValue) { this.changes.model = newValue; requestUpdate(this); } /** * Invoked everytime the bound view changes. * @param newValue The new value. * @param oldValue The old value. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars viewChanged(newValue, oldValue) { this.changes.view = newValue; requestUpdate(this); } /** * Invoked everytime the bound view model changes. * @param newValue The new value. * @param oldValue The old value. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars viewModelChanged(newValue, oldValue) { this.changes.viewModel = newValue; requestUpdate(this); } } function isEmpty(obj) { for (const _ in obj) { return false; } return true; } function tryActivateViewModel(vm, model) { if (vm && typeof vm.activate === 'function') { return Promise.resolve(vm.activate(model)); } } function createInstruction(composer: Compose, instruction: CompositionContext): CompositionContext { return Object.assign(instruction, { bindingContext: composer.bindingContext, overrideContext: composer.overrideContext, owningView: composer.owningView, container: composer.container, viewSlot: composer.viewSlot, viewResources: composer.viewResources, currentController: composer.currentController, host: composer.element, swapOrder: composer.swapOrder }); } function processChanges(composer: Compose) { const changes = composer.changes; composer.changes = Object.create(null); if (needsReInitialization(composer, changes)) { // init context let instruction = { view: composer.view, viewModel: composer.currentViewModel || composer.viewModel, model: composer.model } as CompositionContext; // apply changes instruction = Object.assign(instruction, changes); // create context instruction = createInstruction(composer, instruction); composer.pendingTask = composer.compositionEngine.compose(instruction).then(controller => { composer.currentController = controller; composer.currentViewModel = controller ? controller.viewModel : null; }); } else { // just try to activate the current view model composer.pendingTask = tryActivateViewModel(composer.currentViewModel, changes.model); if (!composer.pendingTask) { return; } } composer.pendingTask = composer.pendingTask .then(() => { completeCompositionTask(composer); }, reason => { completeCompositionTask(composer); throw reason; }); } function completeCompositionTask(composer) { composer.pendingTask = null; if (!isEmpty(composer.changes)) { processChanges(composer); } } function requestUpdate(composer: Compose) { if (composer.pendingTask || composer.updateRequested) { return; } composer.updateRequested = true; composer.taskQueue.queueMicroTask(() => { composer.updateRequested = false; processChanges(composer); }); } function needsReInitialization(composer: Compose, changes: any) { let activationStrategy = composer.activationStrategy; const vm = composer.currentViewModel; if (vm && typeof vm.determineActivationStrategy === 'function') { activationStrategy = vm.determineActivationStrategy(); } return 'view' in changes || 'viewModel' in changes || activationStrategy === ActivationStrategy.Replace; }