aurelia-templating-resources
Version:
A standard set of behaviors, converters and other resources for use with the Aurelia templating library.
316 lines (288 loc) • 8.36 kB
text/typescript
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 `<compose/>` 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.
*/
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}
*/
model: any;
/**
* View to bind the custom element to.
*
* @property view
* @type {HtmlElement}
*/
view: any;
/**
* View-model to bind the custom element's template to.
*
* @property viewModel
* @type {Class}
*/
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}
*/
activationStrategy: ActivationStrategy = ActivationStrategy.InvokeLifecycle;
/**
* SwapOrder to control the swapping order of the custom element's view.
*
* @property view
* @type {String}
*/
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;
}