@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
1,603 lines (1,379 loc) • 187 kB
JavaScript
import * as LogManager from 'aurelia-logging';
import {metadata,Origin,protocol} from 'aurelia-metadata';
import {DOM,PLATFORM,FEATURE} from 'aurelia-pal';
import {relativeToFile} from 'aurelia-path';
import {TemplateRegistryEntry,Loader} from 'aurelia-loader';
import {inject,Container,resolver} from 'aurelia-dependency-injection';
import {Binding,createOverrideContext,ValueConverterResource,BindingBehaviorResource,subscriberCollection,bindingMode,ObserverLocator,EventManager} from 'aurelia-binding';
import {TaskQueue} from 'aurelia-task-queue';
/**
* List the events that an Animator should raise.
*/
export const animationEvent = {
enterBegin: 'animation:enter:begin',
enterActive: 'animation:enter:active',
enterDone: 'animation:enter:done',
enterTimeout: 'animation:enter:timeout',
leaveBegin: 'animation:leave:begin',
leaveActive: 'animation:leave:active',
leaveDone: 'animation:leave:done',
leaveTimeout: 'animation:leave:timeout',
staggerNext: 'animation:stagger:next',
removeClassBegin: 'animation:remove-class:begin',
removeClassActive: 'animation:remove-class:active',
removeClassDone: 'animation:remove-class:done',
removeClassTimeout: 'animation:remove-class:timeout',
addClassBegin: 'animation:add-class:begin',
addClassActive: 'animation:add-class:active',
addClassDone: 'animation:add-class:done',
addClassTimeout: 'animation:add-class:timeout',
animateBegin: 'animation:animate:begin',
animateActive: 'animation:animate:active',
animateDone: 'animation:animate:done',
animateTimeout: 'animation:animate:timeout',
sequenceBegin: 'animation:sequence:begin',
sequenceDone: 'animation:sequence:done'
};
/**
* An abstract class representing a mechanism for animating the DOM during various DOM state transitions.
*/
export class Animator {
/**
* Execute an 'enter' animation on an element
* @param element Element to animate
* @returns Resolved when the animation is done
*/
enter(element: HTMLElement): Promise<boolean> {
return Promise.resolve(false);
}
/**
* Execute a 'leave' animation on an element
* @param element Element to animate
* @returns Resolved when the animation is done
*/
leave(element: HTMLElement): Promise<boolean> {
return Promise.resolve(false);
}
/**
* Add a class to an element to trigger an animation.
* @param element Element to animate
* @param className Properties to animate or name of the effect to use
* @returns Resolved when the animation is done
*/
removeClass(element: HTMLElement, className: string): Promise<boolean> {
element.classList.remove(className);
return Promise.resolve(false);
}
/**
* Add a class to an element to trigger an animation.
* @param element Element to animate
* @param className Properties to animate or name of the effect to use
* @returns Resolved when the animation is done
*/
addClass(element: HTMLElement, className: string): Promise<boolean> {
element.classList.add(className);
return Promise.resolve(false);
}
/**
* Execute a single animation.
* @param element Element to animate
* @param className Properties to animate or name of the effect to use. For css animators this represents the className to be added and removed right after the animation is done.
* @param options options for the animation (duration, easing, ...)
* @returns Resolved when the animation is done
*/
animate(element: HTMLElement | Array<HTMLElement>, className: string): Promise<boolean> {
return Promise.resolve(false);
}
/**
* Run a sequence of animations one after the other.
* for example: animator.runSequence("fadeIn","callout")
* @param sequence An array of effectNames or classNames
* @returns Resolved when all animations are done
*/
runSequence(animations:Array<any>): Promise<boolean> {}
/**
* Register an effect (for JS based animators)
* @param effectName identifier of the effect
* @param properties Object with properties for the effect
*/
registerEffect(effectName: string, properties: Object): void {}
/**
* Unregister an effect (for JS based animators)
* @param effectName identifier of the effect
*/
unregisterEffect(effectName: string): void {}
}
/**
* A mechanism by which an enlisted async render operation can notify the owning transaction when its work is done.
*/
export class CompositionTransactionNotifier {
constructor(owner) {
this.owner = owner;
this.owner._compositionCount++;
}
/**
* Notifies the owning transaction that its work is done.
*/
done(): void {
this.owner._compositionCount--;
this.owner._tryCompleteTransaction();
}
}
/**
* Referenced by the subsytem which wishes to control a composition transaction.
*/
export class CompositionTransactionOwnershipToken {
constructor(owner) {
this.owner = owner;
this.owner._ownershipToken = this;
this.thenable = this._createThenable();
}
/**
* Allows the transaction owner to wait for the completion of all child compositions.
* @return A promise that resolves when all child compositions are done.
*/
waitForCompositionComplete(): Promise<void> {
this.owner._tryCompleteTransaction();
return this.thenable;
}
/**
* Used internall to resolve the composition complete promise.
*/
resolve(): void {
this._resolveCallback();
}
_createThenable() {
return new Promise((resolve, reject) => {
this._resolveCallback = resolve;
});
}
}
/**
* Enables an initiator of a view composition to track any internal async rendering processes for completion.
*/
export class CompositionTransaction {
/**
* Creates an instance of CompositionTransaction.
*/
constructor() {
this._ownershipToken = null;
this._compositionCount = 0;
}
/**
* Attempt to take ownership of the composition transaction.
* @return An ownership token if successful, otherwise null.
*/
tryCapture(): CompositionTransactionOwnershipToken {
return this._ownershipToken === null
? new CompositionTransactionOwnershipToken(this)
: null;
}
/**
* Enlist an async render operation into the transaction.
* @return A completion notifier.
*/
enlist(): CompositionTransactionNotifier {
return new CompositionTransactionNotifier(this);
}
_tryCompleteTransaction() {
if (this._compositionCount <= 0) {
this._compositionCount = 0;
if (this._ownershipToken !== null) {
let token = this._ownershipToken;
this._ownershipToken = null;
token.resolve();
}
}
}
}
const capitalMatcher = /([A-Z])/g;
function addHyphenAndLower(char) {
return '-' + char.toLowerCase();
}
export function _hyphenate(name) {
return (name.charAt(0).toLowerCase() + name.slice(1)).replace(capitalMatcher, addHyphenAndLower);
}
//https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace_in_the_DOM
//We need to ignore whitespace so we don't mess up fallback rendering
//However, we cannot ignore empty text nodes that container interpolations.
export function _isAllWhitespace(node) {
// Use ECMA-262 Edition 3 String and RegExp features
return !(node.auInterpolationTarget || (/[^\t\n\r ]/.test(node.textContent)));
}
export class ViewEngineHooksResource {
constructor() {}
initialize(container, target) {
this.instance = container.get(target);
}
register(registry, name) {
registry.registerViewEngineHooks(this.instance);
}
load(container, target) {}
static convention(name) { // eslint-disable-line
if (name.endsWith('ViewEngineHooks')) {
return new ViewEngineHooksResource();
}
}
}
export function viewEngineHooks(target) { // eslint-disable-line
let deco = function(t) {
metadata.define(metadata.resource, new ViewEngineHooksResource(), t);
};
return target ? deco(target) : deco;
}
interface EventHandler {
eventName: string;
bubbles: boolean;
dispose: Function;
handler: Function;
}
/**
* Dispatches subscribets to and publishes events in the DOM.
* @param element
*/
export class ElementEvents {
constructor(element: Element) {
this.element = element;
this.subscriptions = {};
}
_enqueueHandler(handler: EventHandler): void {
this.subscriptions[handler.eventName] = this.subscriptions[handler.eventName] || [];
this.subscriptions[handler.eventName].push(handler);
}
_dequeueHandler(handler: EventHandler): EventHandler {
let index;
let subscriptions = this.subscriptions[handler.eventName];
if (subscriptions) {
index = subscriptions.indexOf(handler);
if (index > -1) {
subscriptions.splice(index, 1);
}
}
return handler;
}
/**
* Dispatches an Event on the context element.
* @param eventName
* @param detail
* @param bubbles
* @param cancelable
*/
publish(eventName: string, detail?: Object = {}, bubbles?: boolean = true, cancelable?: boolean = true) {
let event = DOM.createCustomEvent(eventName, {cancelable, bubbles, detail});
this.element.dispatchEvent(event);
}
/**
* Adds and Event Listener on the context element.
* @param eventName
* @param handler
* @param bubbles
* @return Returns the eventHandler containing a dispose method
*/
subscribe(eventName: string, handler: Function, bubbles?: boolean = true): EventHandler {
if (handler && typeof handler === 'function') {
handler.eventName = eventName;
handler.handler = handler;
handler.bubbles = bubbles;
handler.dispose = () => {
this.element.removeEventListener(eventName, handler, bubbles);
this._dequeueHandler(handler);
};
this.element.addEventListener(eventName, handler, bubbles);
this._enqueueHandler(handler);
return handler;
}
return undefined;
}
/**
* Adds an Event Listener on the context element, that will be disposed on the first trigger.
* @param eventName
* @param handler
* @param bubbles
* @return Returns the eventHandler containing a dispose method
*/
subscribeOnce(eventName: String, handler: Function, bubbles?: Boolean = true): EventHandler {
if (handler && typeof handler === 'function') {
let _handler = (event) => {
handler(event);
_handler.dispose();
};
return this.subscribe(eventName, _handler, bubbles);
}
return undefined;
}
/**
* Removes all events that are listening to the specified eventName.
* @param eventName
*/
dispose(eventName: string): void {
if (eventName && typeof eventName === 'string') {
let subscriptions = this.subscriptions[eventName];
if (subscriptions) {
while (subscriptions.length) {
let subscription = subscriptions.pop();
if (subscription) {
subscription.dispose();
}
}
}
} else {
this.disposeAll();
}
}
/**
* Removes all event handlers.
*/
disposeAll() {
for (let key in this.subscriptions) {
this.dispose(key);
}
}
}
/**
* A context that flows through the view resource load process.
*/
export class ResourceLoadContext {
dependencies: Object;
/**
* Creates an instance of ResourceLoadContext.
*/
constructor() {
this.dependencies = {};
}
/**
* Tracks a dependency that is being loaded.
* @param url The url of the dependency.
*/
addDependency(url: string): void {
this.dependencies[url] = true;
}
/**
* Checks if the current context includes a load of the specified url.
* @return True if the url is being loaded in the context; false otherwise.
*/
hasDependency(url: string): boolean {
return url in this.dependencies;
}
}
/**
* Specifies how a view should be compiled.
*/
export class ViewCompileInstruction {
targetShadowDOM: boolean;
compileSurrogate: boolean;
associatedModuleId: any;
/**
* The normal configuration for view compilation.
*/
static normal: ViewCompileInstruction;
/**
* Creates an instance of ViewCompileInstruction.
* @param targetShadowDOM Should the compilation target the Shadow DOM.
* @param compileSurrogate Should the compilation also include surrogate bindings and behaviors.
*/
constructor(targetShadowDOM?: boolean = false, compileSurrogate?: boolean = false) {
this.targetShadowDOM = targetShadowDOM;
this.compileSurrogate = compileSurrogate;
this.associatedModuleId = null;
}
}
ViewCompileInstruction.normal = new ViewCompileInstruction();
/**
* Specifies how a view should be created.
*/
interface ViewCreateInstruction {
/**
* Indicates that the view is being created by enhancing existing DOM.
*/
enhance?: boolean;
/**
* Specifies a key/value lookup of part replacements for the view being created.
*/
partReplacements?: Object;
}
/**
* Indicates how a custom attribute or element should be instantiated in a view.
*/
export class BehaviorInstruction {
initiatedByBehavior: boolean;
enhance: boolean;
partReplacements: any;
viewFactory: ViewFactory;
originalAttrName: string;
skipContentProcessing: boolean;
contentFactory: any;
viewModel: Object;
anchorIsContainer: boolean;
host: Element;
attributes: Object;
type: HtmlBehaviorResource;
attrName: string;
inheritBindingContext: boolean;
/**
* A default behavior used in scenarios where explicit configuration isn't available.
*/
static normal: BehaviorInstruction;
/**
* Creates an instruction for element enhancement.
* @return The created instruction.
*/
static enhance(): BehaviorInstruction {
let instruction = new BehaviorInstruction();
instruction.enhance = true;
return instruction;
}
/**
* Creates an instruction for unit testing.
* @param type The HtmlBehaviorResource to create.
* @param attributes A key/value lookup of attributes for the behaior.
* @return The created instruction.
*/
static unitTest(type: HtmlBehaviorResource, attributes: Object): BehaviorInstruction {
let instruction = new BehaviorInstruction();
instruction.type = type;
instruction.attributes = attributes || {};
return instruction;
}
/**
* Creates a custom element instruction.
* @param node The node that represents the custom element.
* @param type The HtmlBehaviorResource to create.
* @return The created instruction.
*/
static element(node: Node, type: HtmlBehaviorResource): BehaviorInstruction {
let instruction = new BehaviorInstruction();
instruction.type = type;
instruction.attributes = {};
instruction.anchorIsContainer = !(node.hasAttribute('containerless') || type.containerless);
instruction.initiatedByBehavior = true;
return instruction;
}
/**
* Creates a custom attribute instruction.
* @param attrName The name of the attribute.
* @param type The HtmlBehaviorResource to create.
* @return The created instruction.
*/
static attribute(attrName: string, type?: HtmlBehaviorResource): BehaviorInstruction {
let instruction = new BehaviorInstruction();
instruction.attrName = attrName;
instruction.type = type || null;
instruction.attributes = {};
return instruction;
}
/**
* Creates a dynamic component instruction.
* @param host The element that will parent the dynamic component.
* @param viewModel The dynamic component's view model instance.
* @param viewFactory A view factory used in generating the component's view.
* @return The created instruction.
*/
static dynamic(host: Element, viewModel: Object, viewFactory: ViewFactory): BehaviorInstruction {
let instruction = new BehaviorInstruction();
instruction.host = host;
instruction.viewModel = viewModel;
instruction.viewFactory = viewFactory;
instruction.inheritBindingContext = true;
return instruction;
}
/**
* Creates an instance of BehaviorInstruction.
*/
constructor() {
this.initiatedByBehavior = false;
this.enhance = false;
this.partReplacements = null;
this.viewFactory = null;
this.originalAttrName = null;
this.skipContentProcessing = false;
this.contentFactory = null;
this.viewModel = null;
this.anchorIsContainer = false;
this.host = null;
this.attributes = null;
this.type = null;
this.attrName = null;
this.inheritBindingContext = false;
}
}
BehaviorInstruction.normal = new BehaviorInstruction();
/**
* Provides all the instructions for how a target element should be enhanced inside of a view.
*/
export class TargetInstruction {
injectorId:number;
parentInjectorId:number;
shadowSlot:boolean;
slotName:string;
slotFallbackFactory:any;
contentExpression:any;
expressions:Array<Object>;
behaviorInstructions:Array<BehaviorInstruction>;
providers:Array<Function>;
viewFactory:ViewFactory;
anchorIsContainer:boolean;
elementInstruction:BehaviorInstruction;
lifting:boolean;
values:Object;
/**
* An empty array used to represent a target with no binding expressions.
*/
static noExpressions = Object.freeze([]);
/**
* Creates an instruction that represents a shadow dom slot.
* @param parentInjectorId The id of the parent dependency injection container.
* @return The created instruction.
*/
static shadowSlot(parentInjectorId: number): TargetInstruction {
let instruction = new TargetInstruction();
instruction.parentInjectorId = parentInjectorId;
instruction.shadowSlot = true;
return instruction;
}
/**
* Creates an instruction that represents a binding expression in the content of an element.
* @param expression The binding expression.
* @return The created instruction.
*/
static contentExpression(expression): TargetInstruction {
let instruction = new TargetInstruction();
instruction.contentExpression = expression;
return instruction;
}
/**
* Creates an instruction that represents content that was lifted out of the DOM and into a ViewFactory.
* @param parentInjectorId The id of the parent dependency injection container.
* @param liftingInstruction The behavior instruction of the lifting behavior.
* @return The created instruction.
*/
static lifting(parentInjectorId: number, liftingInstruction: BehaviorInstruction): TargetInstruction {
let instruction = new TargetInstruction();
instruction.parentInjectorId = parentInjectorId;
instruction.expressions = TargetInstruction.noExpressions;
instruction.behaviorInstructions = [liftingInstruction];
instruction.viewFactory = liftingInstruction.viewFactory;
instruction.providers = [liftingInstruction.type.target];
instruction.lifting = true;
return instruction;
}
/**
* Creates an instruction that represents an element with behaviors and bindings.
* @param injectorId The id of the dependency injection container.
* @param parentInjectorId The id of the parent dependency injection container.
* @param providers The types which will provide behavior for this element.
* @param behaviorInstructions The instructions for creating behaviors on this element.
* @param expressions Bindings, listeners, triggers, etc.
* @param elementInstruction The element behavior for this element.
* @return The created instruction.
*/
static normal(injectorId: number, parentInjectorId: number, providers: Array<Function>, behaviorInstructions: Array<BehaviorInstruction>, expressions: Array<Object>, elementInstruction: BehaviorInstruction): TargetInstruction {
let instruction = new TargetInstruction();
instruction.injectorId = injectorId;
instruction.parentInjectorId = parentInjectorId;
instruction.providers = providers;
instruction.behaviorInstructions = behaviorInstructions;
instruction.expressions = expressions;
instruction.anchorIsContainer = elementInstruction ? elementInstruction.anchorIsContainer : true;
instruction.elementInstruction = elementInstruction;
return instruction;
}
/**
* Creates an instruction that represents the surrogate behaviors and bindings for an element.
* @param providers The types which will provide behavior for this element.
* @param behaviorInstructions The instructions for creating behaviors on this element.
* @param expressions Bindings, listeners, triggers, etc.
* @param values A key/value lookup of attributes to transplant.
* @return The created instruction.
*/
static surrogate(providers: Array<Function>, behaviorInstructions: Array<BehaviorInstruction>, expressions: Array<Object>, values: Object): TargetInstruction {
let instruction = new TargetInstruction();
instruction.expressions = expressions;
instruction.behaviorInstructions = behaviorInstructions;
instruction.providers = providers;
instruction.values = values;
return instruction;
}
/**
* Creates an instance of TargetInstruction.
*/
constructor() {
this.injectorId = null;
this.parentInjectorId = null;
this.shadowSlot = false;
this.slotName = null;
this.slotFallbackFactory = null;
this.contentExpression = null;
this.expressions = null;
this.behaviorInstructions = null;
this.providers = null;
this.viewFactory = null;
this.anchorIsContainer = false;
this.elementInstruction = null;
this.lifting = false;
this.values = null;
}
}
/**
* Implemented by classes that describe how a view factory should be loaded.
*/
interface ViewStrategy {
/**
* Loads a view factory.
* @param viewEngine The view engine to use during the load process.
* @param compileInstruction Additional instructions to use during compilation of the view.
* @param loadContext The loading context used for loading all resources and dependencies.
* @param target A class from which to extract metadata of additional resources to load.
* @return A promise for the view factory that is produced by this strategy.
*/
loadViewFactory(viewEngine: ViewEngine, compileInstruction: ViewCompileInstruction, loadContext?: ResourceLoadContext, target?: any): Promise<ViewFactory>;
}
/**
* Decorator: Indicates that the decorated class/object is a view strategy.
*/
export const viewStrategy: Function = protocol.create('aurelia:view-strategy', {
validate(target) {
if (!(typeof target.loadViewFactory === 'function')) {
return 'View strategies must implement: loadViewFactory(viewEngine: ViewEngine, compileInstruction: ViewCompileInstruction, loadContext?: ResourceLoadContext): Promise<ViewFactory>';
}
return true;
},
compose(target) {
if (!(typeof target.makeRelativeTo === 'function')) {
target.makeRelativeTo = PLATFORM.noop;
}
}
});
/**
* A view strategy that loads a view relative to its associated view-model.
*/
@viewStrategy()
export class RelativeViewStrategy {
/**
* Creates an instance of RelativeViewStrategy.
* @param path The relative path to the view.
*/
constructor(path: string) {
this.path = path;
this.absolutePath = null;
}
/**
* Loads a view factory.
* @param viewEngine The view engine to use during the load process.
* @param compileInstruction Additional instructions to use during compilation of the view.
* @param loadContext The loading context used for loading all resources and dependencies.
* @param target A class from which to extract metadata of additional resources to load.
* @return A promise for the view factory that is produced by this strategy.
*/
loadViewFactory(viewEngine: ViewEngine, compileInstruction: ViewCompileInstruction, loadContext?: ResourceLoadContext, target?: any): Promise<ViewFactory> {
if (this.absolutePath === null && this.moduleId) {
this.absolutePath = relativeToFile(this.path, this.moduleId);
}
compileInstruction.associatedModuleId = this.moduleId;
return viewEngine.loadViewFactory(this.absolutePath || this.path, compileInstruction, loadContext, target);
}
/**
* Makes the view loaded by this strategy relative to the provided file path.
* @param file The path to load the view relative to.
*/
makeRelativeTo(file: string): void {
if (this.absolutePath === null) {
this.absolutePath = relativeToFile(this.path, file);
}
}
}
/**
* A view strategy based on naming conventions.
*/
@viewStrategy()
export class ConventionalViewStrategy {
/**
* Creates an instance of ConventionalViewStrategy.
* @param viewLocator The view locator service for conventionally locating the view.
* @param origin The origin of the view model to conventionally load the view for.
*/
constructor(viewLocator: ViewLocator, origin: Origin) {
this.moduleId = origin.moduleId;
this.viewUrl = viewLocator.convertOriginToViewUrl(origin);
}
/**
* Loads a view factory.
* @param viewEngine The view engine to use during the load process.
* @param compileInstruction Additional instructions to use during compilation of the view.
* @param loadContext The loading context used for loading all resources and dependencies.
* @param target A class from which to extract metadata of additional resources to load.
* @return A promise for the view factory that is produced by this strategy.
*/
loadViewFactory(viewEngine: ViewEngine, compileInstruction: ViewCompileInstruction, loadContext?: ResourceLoadContext, target?: any): Promise<ViewFactory> {
compileInstruction.associatedModuleId = this.moduleId;
return viewEngine.loadViewFactory(this.viewUrl, compileInstruction, loadContext, target);
}
}
/**
* A view strategy that indicates that the component has no view that the templating engine needs to manage.
* Typically used when the component author wishes to take over fine-grained rendering control.
*/
@viewStrategy()
export class NoViewStrategy {
/**
* Creates an instance of NoViewStrategy.
* @param dependencies A list of view resource dependencies of this view.
* @param dependencyBaseUrl The base url for the view dependencies.
*/
constructor(dependencies?: Array<string|Function|Object>, dependencyBaseUrl?: string) {
this.dependencies = dependencies || null;
this.dependencyBaseUrl = dependencyBaseUrl || '';
}
/**
* Loads a view factory.
* @param viewEngine The view engine to use during the load process.
* @param compileInstruction Additional instructions to use during compilation of the view.
* @param loadContext The loading context used for loading all resources and dependencies.
* @param target A class from which to extract metadata of additional resources to load.
* @return A promise for the view factory that is produced by this strategy.
*/
loadViewFactory(viewEngine: ViewEngine, compileInstruction: ViewCompileInstruction, loadContext?: ResourceLoadContext, target?: any): Promise<ViewFactory> {
let entry = this.entry;
let dependencies = this.dependencies;
if (entry && entry.factoryIsReady) {
return Promise.resolve(null);
}
this.entry = entry = new TemplateRegistryEntry(this.moduleId || this.dependencyBaseUrl);
// since we're not invoking the TemplateRegistryEntry template setter
// we need to create the dependencies Array manually and set it as loaded:
entry.dependencies = [];
entry.templateIsLoaded = true;
if (dependencies !== null) {
for (let i = 0, ii = dependencies.length; i < ii; ++i) {
let current = dependencies[i];
if (typeof current === 'string' || typeof current === 'function') {
entry.addDependency(current);
} else {
entry.addDependency(current.from, current.as);
}
}
}
compileInstruction.associatedModuleId = this.moduleId;
// loadViewFactory will resolve as 'null' because entry template is not set:
return viewEngine.loadViewFactory(entry, compileInstruction, loadContext, target);
}
}
/**
* A view strategy created directly from the template registry entry.
*/
@viewStrategy()
export class TemplateRegistryViewStrategy {
/**
* Creates an instance of TemplateRegistryViewStrategy.
* @param moduleId The associated moduleId of the view to be loaded.
* @param entry The template registry entry used in loading the view factory.
*/
constructor(moduleId: string, entry: TemplateRegistryEntry) {
this.moduleId = moduleId;
this.entry = entry;
}
/**
* Loads a view factory.
* @param viewEngine The view engine to use during the load process.
* @param compileInstruction Additional instructions to use during compilation of the view.
* @param loadContext The loading context used for loading all resources and dependencies.
* @param target A class from which to extract metadata of additional resources to load.
* @return A promise for the view factory that is produced by this strategy.
*/
loadViewFactory(viewEngine: ViewEngine, compileInstruction: ViewCompileInstruction, loadContext?: ResourceLoadContext, target?: any): Promise<ViewFactory> {
let entry = this.entry;
if (entry.factoryIsReady) {
return Promise.resolve(entry.factory);
}
compileInstruction.associatedModuleId = this.moduleId;
return viewEngine.loadViewFactory(entry, compileInstruction, loadContext, target);
}
}
/**
* A view strategy that allows the component authore to inline the html for the view.
*/
@viewStrategy()
export class InlineViewStrategy {
/**
* Creates an instance of InlineViewStrategy.
* @param markup The markup for the view. Be sure to include the wrapping template tag.
* @param dependencies A list of view resource dependencies of this view.
* @param dependencyBaseUrl The base url for the view dependencies.
*/
constructor(markup: string, dependencies?: Array<string|Function|Object>, dependencyBaseUrl?: string) {
this.markup = markup;
this.dependencies = dependencies || null;
this.dependencyBaseUrl = dependencyBaseUrl || '';
}
/**
* Loads a view factory.
* @param viewEngine The view engine to use during the load process.
* @param compileInstruction Additional instructions to use during compilation of the view.
* @param loadContext The loading context used for loading all resources and dependencies.
* @param target A class from which to extract metadata of additional resources to load.
* @return A promise for the view factory that is produced by this strategy.
*/
loadViewFactory(viewEngine: ViewEngine, compileInstruction: ViewCompileInstruction, loadContext?: ResourceLoadContext, target?: any): Promise<ViewFactory> {
let entry = this.entry;
let dependencies = this.dependencies;
if (entry && entry.factoryIsReady) {
return Promise.resolve(entry.factory);
}
this.entry = entry = new TemplateRegistryEntry(this.moduleId || this.dependencyBaseUrl);
entry.template = DOM.createTemplateFromMarkup(this.markup);
if (dependencies !== null) {
for (let i = 0, ii = dependencies.length; i < ii; ++i) {
let current = dependencies[i];
if (typeof current === 'string' || typeof current === 'function') {
entry.addDependency(current);
} else {
entry.addDependency(current.from, current.as);
}
}
}
compileInstruction.associatedModuleId = this.moduleId;
return viewEngine.loadViewFactory(entry, compileInstruction, loadContext, target);
}
}
/**
* Locates a view for an object.
*/
export class ViewLocator {
/**
* The metadata key for storing/finding view strategies associated with an class/object.
*/
static viewStrategyMetadataKey = 'aurelia:view-strategy';
/**
* Gets the view strategy for the value.
* @param value The value to locate the view strategy for.
* @return The located ViewStrategy instance.
*/
getViewStrategy(value: any): ViewStrategy {
if (!value) {
return null;
}
if (typeof value === 'object' && 'getViewStrategy' in value) {
let origin = Origin.get(value.constructor);
value = value.getViewStrategy();
if (typeof value === 'string') {
value = new RelativeViewStrategy(value);
}
viewStrategy.assert(value);
if (origin.moduleId) {
value.makeRelativeTo(origin.moduleId);
}
return value;
}
if (typeof value === 'string') {
value = new RelativeViewStrategy(value);
}
if (viewStrategy.validate(value)) {
return value;
}
if (typeof value !== 'function') {
value = value.constructor;
}
let origin = Origin.get(value);
let strategy = metadata.get(ViewLocator.viewStrategyMetadataKey, value);
if (!strategy) {
if (!origin.moduleId) {
throw new Error('Cannot determine default view strategy for object.', value);
}
strategy = this.createFallbackViewStrategy(origin);
} else if (origin.moduleId) {
strategy.moduleId = origin.moduleId;
}
return strategy;
}
/**
* Creates a fallback View Strategy. Used when unable to locate a configured strategy.
* The default implementation returns and instance of ConventionalViewStrategy.
* @param origin The origin of the view model to return the strategy for.
* @return The fallback ViewStrategy.
*/
createFallbackViewStrategy(origin: Origin): ViewStrategy {
return new ConventionalViewStrategy(this, origin);
}
/**
* Conventionally converts a view model origin to a view url.
* Used by the ConventionalViewStrategy.
* @param origin The origin of the view model to convert.
* @return The view url.
*/
convertOriginToViewUrl(origin: Origin): string {
let moduleId = origin.moduleId;
let id = (moduleId.endsWith('.js') || moduleId.endsWith('.ts')) ? moduleId.substring(0, moduleId.length - 3) : moduleId;
return id + '.html';
}
}
function mi(name) {
throw new Error(`BindingLanguage must implement ${name}().`);
}
/**
* An abstract base class for implementations of a binding language.
*/
export class BindingLanguage {
/**
* Inspects an attribute for bindings.
* @param resources The ViewResources for the view being compiled.
* @param elementName The element name to inspect.
* @param attrName The attribute name to inspect.
* @param attrValue The attribute value to inspect.
* @return An info object with the results of the inspection.
*/
inspectAttribute(resources: ViewResources, elementName: string, attrName: string, attrValue: string): Object {
mi('inspectAttribute');
}
/**
* Creates an attribute behavior instruction.
* @param resources The ViewResources for the view being compiled.
* @param element The element that the attribute is defined on.
* @param info The info object previously returned from inspectAttribute.
* @param existingInstruction A previously created instruction for this attribute.
* @return The instruction instance.
*/
createAttributeInstruction(resources: ViewResources, element: Element, info: Object, existingInstruction?: Object): BehaviorInstruction {
mi('createAttributeInstruction');
}
/**
* Parses the text for bindings.
* @param resources The ViewResources for the view being compiled.
* @param value The value of the text to parse.
* @return A binding expression.
*/
inspectTextContent(resources: ViewResources, value: string): Object {
mi('inspectTextContent');
}
}
let noNodes = Object.freeze([]);
@inject(DOM.Element)
export class SlotCustomAttribute {
constructor(element) {
this.element = element;
this.element.auSlotAttribute = this;
}
valueChanged(newValue, oldValue) {
//console.log('au-slot', newValue);
}
}
export class PassThroughSlot {
constructor(anchor, name, destinationName, fallbackFactory) {
this.anchor = anchor;
this.anchor.viewSlot = this;
this.name = name;
this.destinationName = destinationName;
this.fallbackFactory = fallbackFactory;
this.destinationSlot = null;
this.projections = 0;
this.contentView = null;
let attr = new SlotCustomAttribute(this.anchor);
attr.value = this.destinationName;
}
get needsFallbackRendering() {
return this.fallbackFactory && this.projections === 0;
}
renderFallbackContent(view, nodes, projectionSource, index) {
if (this.contentView === null) {
this.contentView = this.fallbackFactory.create(this.ownerView.container);
this.contentView.bind(this.ownerView.bindingContext, this.ownerView.overrideContext);
let slots = Object.create(null);
slots[this.destinationSlot.name] = this.destinationSlot;
ShadowDOM.distributeView(this.contentView, slots, projectionSource, index, this.destinationSlot.name);
}
}
passThroughTo(destinationSlot) {
this.destinationSlot = destinationSlot;
}
addNode(view, node, projectionSource, index) {
if (this.contentView !== null) {
this.contentView.removeNodes();
this.contentView.detached();
this.contentView.unbind();
this.contentView = null;
}
if (node.viewSlot instanceof PassThroughSlot) {
node.viewSlot.passThroughTo(this);
return;
}
this.projections++;
this.destinationSlot.addNode(view, node, projectionSource, index);
}
removeView(view, projectionSource) {
this.projections--;
this.destinationSlot.removeView(view, projectionSource);
if (this.needsFallbackRendering) {
this.renderFallbackContent(null, noNodes, projectionSource);
}
}
removeAll(projectionSource) {
this.projections = 0;
this.destinationSlot.removeAll(projectionSource);
if (this.needsFallbackRendering) {
this.renderFallbackContent(null, noNodes, projectionSource);
}
}
projectFrom(view, projectionSource) {
this.destinationSlot.projectFrom(view, projectionSource);
}
created(ownerView) {
this.ownerView = ownerView;
}
bind(view) {
if (this.contentView) {
this.contentView.bind(view.bindingContext, view.overrideContext);
}
}
attached() {
if (this.contentView) {
this.contentView.attached();
}
}
detached() {
if (this.contentView) {
this.contentView.detached();
}
}
unbind() {
if (this.contentView) {
this.contentView.unbind();
}
}
}
export class ShadowSlot {
constructor(anchor, name, fallbackFactory) {
this.anchor = anchor;
this.anchor.isContentProjectionSource = true;
this.anchor.viewSlot = this;
this.name = name;
this.fallbackFactory = fallbackFactory;
this.contentView = null;
this.projections = 0;
this.children = [];
this.projectFromAnchors = null;
this.destinationSlots = null;
}
get needsFallbackRendering() {
return this.fallbackFactory && this.projections === 0;
}
addNode(view, node, projectionSource, index, destination) {
if (this.contentView !== null) {
this.contentView.removeNodes();
this.contentView.detached();
this.contentView.unbind();
this.contentView = null;
}
if (node.viewSlot instanceof PassThroughSlot) {
node.viewSlot.passThroughTo(this);
return;
}
if (this.destinationSlots !== null) {
ShadowDOM.distributeNodes(view, [node], this.destinationSlots, this, index);
} else {
node.auOwnerView = view;
node.auProjectionSource = projectionSource;
node.auAssignedSlot = this;
let anchor = this._findAnchor(view, node, projectionSource, index);
let parent = anchor.parentNode;
parent.insertBefore(node, anchor);
this.children.push(node);
this.projections++;
}
}
removeView(view, projectionSource) {
if (this.destinationSlots !== null) {
ShadowDOM.undistributeView(view, this.destinationSlots, this);
} else if (this.contentView && this.contentView.hasSlots) {
ShadowDOM.undistributeView(view, this.contentView.slots, projectionSource);
} else {
let found = this.children.find(x => x.auSlotProjectFrom === projectionSource);
if (found) {
let children = found.auProjectionChildren;
for (let i = 0, ii = children.length; i < ii; ++i) {
let child = children[i];
if (child.auOwnerView === view) {
children.splice(i, 1);
view.fragment.appendChild(child);
i--; ii--;
this.projections--;
}
}
if (this.needsFallbackRendering) {
this.renderFallbackContent(view, noNodes, projectionSource);
}
}
}
}
removeAll(projectionSource) {
if (this.destinationSlots !== null) {
ShadowDOM.undistributeAll(this.destinationSlots, this);
} else if (this.contentView && this.contentView.hasSlots) {
ShadowDOM.undistributeAll(this.contentView.slots, projectionSource);
} else {
let found = this.children.find(x => x.auSlotProjectFrom === projectionSource);
if (found) {
let children = found.auProjectionChildren;
for (let i = 0, ii = children.length; i < ii; ++i) {
let child = children[i];
child.auOwnerView.fragment.appendChild(child);
this.projections--;
}
found.auProjectionChildren = [];
if (this.needsFallbackRendering) {
this.renderFallbackContent(null, noNodes, projectionSource);
}
}
}
}
_findAnchor(view, node, projectionSource, index) {
if (projectionSource) {
//find the anchor associated with the projected view slot
let found = this.children.find(x => x.auSlotProjectFrom === projectionSource);
if (found) {
if (index !== undefined) {
let children = found.auProjectionChildren;
let viewIndex = -1;
let lastView;
for (let i = 0, ii = children.length; i < ii; ++i) {
let current = children[i];
if (current.auOwnerView !== lastView) {
viewIndex++;
lastView = current.auOwnerView;
if (viewIndex >= index && lastView !== view) {
children.splice(i, 0, node);
return current;
}
}
}
}
found.auProjectionChildren.push(node);
return found;
}
}
return this.anchor;
}
projectTo(slots) {
this.destinationSlots = slots;
}
projectFrom(view, projectionSource) {
let anchor = DOM.createComment('anchor');
let parent = this.anchor.parentNode;
anchor.auSlotProjectFrom = projectionSource;
anchor.auOwnerView = view;
anchor.auProjectionChildren = [];
parent.insertBefore(anchor, this.anchor);
this.children.push(anchor);
if (this.projectFromAnchors === null) {
this.projectFromAnchors = [];
}
this.projectFromAnchors.push(anchor);
}
renderFallbackContent(view, nodes, projectionSource, index) {
if (this.contentView === null) {
this.contentView = this.fallbackFactory.create(this.ownerView.container);
this.contentView.bind(this.ownerView.bindingContext, this.ownerView.overrideContext);
this.contentView.insertNodesBefore(this.anchor);
}
if (this.contentView.hasSlots) {
let slots = this.contentView.slots;
let projectFromAnchors = this.projectFromAnchors;
if (projectFromAnchors !== null) {
for (let slotName in slots) {
let slot = slots[slotName];
for (let i = 0, ii = projectFromAnchors.length; i < ii; ++i) {
let anchor = projectFromAnchors[i];
slot.projectFrom(anchor.auOwnerView, anchor.auSlotProjectFrom);
}
}
}
this.fallbackSlots = slots;
ShadowDOM.distributeNodes(view, nodes, slots, projectionSource, index);
}
}
created(ownerView) {
this.ownerView = ownerView;
}
bind(view) {
if (this.contentView) {
this.contentView.bind(view.bindingContext, view.overrideContext);
}
}
attached() {
if (this.contentView) {
this.contentView.attached();
}
}
detached() {
if (this.contentView) {
this.contentView.detached();
}
}
unbind() {
if (this.contentView) {
this.contentView.unbind();
}
}
}
export class ShadowDOM {
static defaultSlotKey = '__au-default-slot-key__';
static getSlotName(node) {
if (node.auSlotAttribute === undefined) {
return ShadowDOM.defaultSlotKey;
}
return node.auSlotAttribute.value;
}
static distributeView(view, slots, projectionSource, index, destinationOverride) {
let nodes;
if (view === null) {
nodes = noNodes;
} else {
let childNodes = view.fragment.childNodes;
let ii = childNodes.length;
nodes = new Array(ii);
for (let i = 0; i < ii; ++i) {
nodes[i] = childNodes[i];
}
}
ShadowDOM.distributeNodes(
view,
nodes,
slots,
projectionSource,
index,
destinationOverride
);
}
static undistributeView(view, slots, projectionSource) {
for (let slotName in slots) {
slots[slotName].removeView(view, projectionSource);
}
}
static undistributeAll(slots, projectionSource) {
for (let slotName in slots) {
slots[slotName].removeAll(projectionSource);
}
}
static distributeNodes(view, nodes, slots, projectionSource, index, destinationOverride) {
for (let i = 0, ii = nodes.length; i < ii; ++i) {
let currentNode = nodes[i];
let nodeType = currentNode.nodeType;
if (currentNode.isContentProjectionSource) {
currentNode.viewSlot.projectTo(slots);
for (let slotName in slots) {
slots[slotName].projectFrom(view, currentNode.viewSlot);
}
nodes.splice(i, 1);
ii--; i--;
} else if (nodeType === 1 || nodeType === 3 || currentNode.viewSlot instanceof PassThroughSlot) { //project only elements and text
if (nodeType === 3 && _isAllWhitespace(currentNode)) {
nodes.splice(i, 1);
ii--; i--;
} else {
let found = slots[destinationOverride || ShadowDOM.getSlotName(currentNode)];
if (found) {
found.addNode(view, currentNode, projectionSource, index);
nodes.splice(i, 1);
ii--; i--;
}
}
} else {
nodes.splice(i, 1);
ii--; i--;
}
}
for (let slotName in slots) {
let slot = slots[slotName];
if (slot.needsFallbackRendering) {
slot.renderFallbackContent(view, nodes, projectionSource, index);
}
}
}
}
function register(lookup, name, resource, type) {
if (!name) {
return;
}
let existing = lookup[name];
if (existing) {
if (existing !== resource) {
throw new Error(`Attempted to register ${type} when one with the same name already exists. Name: ${name}.`);
}
return;
}
lookup[name] = resource;
}
/**
* View engine hooks that enable a view resource to provide custom processing during the compilation or creation of a view.
*/
interface ViewEngineHooks {
/**
* Invoked before a template is compiled.
* @param content The DocumentFragment to compile.
* @param resources The resources to compile the view against.
* @param instruction The compilation instruction associated with the compilation process.
*/
beforeCompile?: (content: DocumentFragment, resources: ViewResources, instruction: ViewCompileInstruction) => void;
/**
* Invoked after a template is compiled.
* @param viewFactory The view factory that was produced from the compilation process.
*/
afterCompile?: (viewFactory: ViewFactory) => void;
/**
* Invoked before a view is created.
* @param viewFactory The view factory that will be used to create the view.
* @param container The DI container used during view creation.
* @param content The cloned document fragment representing the view.
* @param instruction The view creation instruction associated with this creation process.
*/
beforeCreate?: (viewFactory: ViewFactory, container: Container, content: DocumentFragment, instruction: ViewCreateInstruction) => void;
/**
* Invoked after a view is created.
* @param view The view that was created by the factory.
*/
afterCreate?: (view: View) => void;
/**
* Invoked after the bindingContext and overrideContext are configured on the view but before the view is bound.
* @param view The view that was created by the factory.
*/
beforeBind?: (view: View) => void;
/**
* Invoked before the view is unbind. The bindingContext and overrideContext are still available on the view.
* @param view The view that was created by the factory.
*/
beforeUnbind?: (view: View) => void;
}
/**
* Represents a collection of resources used during the compilation of a view.
*/
export class ViewResources {
/**
* A custom binding language used in the view.
*/
bindingLanguage = null;
/**
* Creates an instance of ViewResources.
* @param parent The parent resources. This resources can override them, but if a resource is not found, it will be looked up in the parent.
* @param viewUrl The url of the view to which these resources apply.
*/
constructor(parent?: ViewResources, viewUrl?: string) {
this.parent = parent || null;
this.hasParent = this.parent !== null;
this.viewUrl = viewUrl || '';
this.lookupFunctions = {
valueConverters: this.getValueConverter.bind(this),
bindingBehaviors: this.getBindingBehavior.bind(this)
};
this.attributes = Object.create(null);
this.elements = Object.create(null);
this.valueConverters = Object.create(null);
this.bindingBehaviors = Object.create(null);
this.attributeMap = Object.create(null);
this.values = Object.create(null);
this.beforeCompile = this.afterCompile = this.beforeCreate = this.afterCreate = this.beforeBind = this.beforeUnbind = false;
}
_tryAddHook(obj, name) {
if (typeof obj[name] === 'function') {
let func = obj[name].bind(obj);
let counter = 1;
let callbackName;
while (this[callbackName = name + counter.toString()] !== undefined) {
counter++;
}