aurelia-templating
Version:
An extensible HTML templating engine supporting databinding, custom elements, attached behaviors and more.
296 lines (261 loc) • 10.8 kB
text/typescript
import {SwapStrategies} from './swap-strategies';
import {ViewLocator} from './view-locator';
import {ViewEngine} from './view-engine';
import {HtmlBehaviorResource} from './html-behavior';
import {BehaviorInstruction, ViewCompileInstruction} from './instructions';
import {CompositionTransaction, CompositionTransactionNotifier, CompositionTransactionOwnershipToken} from './composition-transaction';
import {DOM} from 'aurelia-pal';
import {Container, inject} from 'aurelia-dependency-injection';
import {metadata} from 'aurelia-metadata';
import { Controller } from './controller';
import { View } from './view';
import { ViewResources } from './view-resources';
import { ViewSlot } from './view-slot';
import { RelativeViewStrategy, ViewStrategy } from './view-strategy';
import { ResourceDescription } from './module-analyzer';
/**
* Instructs the composition engine how to dynamically compose a component.
*/
export 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?: ResourceDescription;
/**
* 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;
/**
* The element that will parent the dynamic component.
* It will be registered in the child container of this composition.
*/
host?: Element;
/**
* Used to declare a notifier when this composition is the owner of the composition chain
* @internal
*/
compositionTransactionOwnershipToken?: CompositionTransactionOwnershipToken;
/**
* Used to declare a notifier when this composition is a part of a composition chain
* @internal
*/
compositionTransactionNotifier?: CompositionTransactionNotifier;
}
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.
*/
export class CompositionEngine {
/** @internal */
viewEngine: ViewEngine;
/** @internal */
private viewLocator: ViewLocator;
/**
* Creates an instance of the CompositionEngine.
* @param viewEngine The ViewEngine used during composition.
*/
constructor(viewEngine: ViewEngine, viewLocator: ViewLocator) {
this.viewEngine = viewEngine;
this.viewLocator = viewLocator;
}
/** @internal */
_swap(context, view) {
let swapStrategy = SwapStrategies[context.swapOrder] || SwapStrategies.after;
let previousViews = context.viewSlot.children.slice();
return swapStrategy(context.viewSlot, previousViews, () => {
return Promise.resolve(context.viewSlot.add(view)).then(() => {
if (context.currentController) {
context.currentController.unbind();
}
});
}).then(() => {
if (context.compositionTransactionNotifier) {
context.compositionTransactionNotifier.done();
}
});
}
/** @internal */
_createControllerAndSwap(context: CompositionContext) {
return this.createController(context).then(controller => {
if (context.compositionTransactionOwnershipToken) {
return context.compositionTransactionOwnershipToken
.waitForCompositionComplete()
.then(() => {
controller.automate(context.overrideContext, context.owningView);
return this._swap(context, controller.view);
})
.then(() => controller);
}
controller.automate(context.overrideContext, context.owningView);
return this._swap(context, controller.view).then(() => 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;
/**@type {HtmlBehaviorResource} */
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 as RelativeViewStrategy).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;
});
}
// When viewModel in context is not a module path
// only prepare the metadata and ensure the view model instance is ready
// if viewModel is a class, instantiate it
let ctor = context.viewModel.constructor;
let isClass = typeof context.viewModel === 'function';
if (isClass) {
ctor = context.viewModel;
childContainer.autoRegister(ctor);
}
let m = metadata.getOrCreateOwn(metadata.resource, HtmlBehaviorResource, ctor) as HtmlBehaviorResource;
// We don't call ViewResources.prototype.convention here as it should be called later
// Just need to prepare the metadata for later usage
m.elementName = m.elementName || 'dynamic-element';
// HtmlBehaviorResource has its own guard to prevent unnecessary subsequent initialization calls
// so it's safe to call initialize this way
m.initialize(isClass ? childContainer : (context.container || childContainer), ctor);
// simulate the metadata of view model, like it was analyzed by module analyzer
// Cannot create a ResourceDescription instance here as it does too much
context.viewModelResource = { metadata: m, value: ctor } as ResourceDescription;
// register the host element in case custom element view model declares it
if (context.host) {
childContainer.registerInstance(DOM.Element, context.host);
}
childContainer.viewModel = context.viewModel = isClass ? childContainer.get(ctor) : 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) as 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 as RelativeViewStrategy).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);
if (context.compositionTransactionOwnershipToken) {
return context.compositionTransactionOwnershipToken.waitForCompositionComplete()
.then(() => this._swap(context, result))
.then(() => result);
}
return this._swap(context, result).then(() => result);
});
} else if (context.viewSlot) {
context.viewSlot.removeAll();
if (context.compositionTransactionNotifier) {
context.compositionTransactionNotifier.done();
}
return Promise.resolve(null);
}
return Promise.resolve(null);
}
}