UNPKG

@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
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++; }