UNPKG

@danielkalen/simplybind

Version:

Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.

287 lines (251 loc) 9.47 kB
import {relativeToFile} from 'aurelia-path'; import {HtmlBehaviorResource} from './html-behavior'; import {BindingLanguage} from './binding-language'; import {ViewCompileInstruction, ViewCreateInstruction} from './instructions'; 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++; } this[name] = true; this[callbackName] = func; } } _invokeHook(name, one, two, three, four) { if (this.hasParent) { this.parent._invokeHook(name, one, two, three, four); } if (this[name]) { this[name + '1'](one, two, three, four); let callbackName = name + '2'; if (this[callbackName]) { this[callbackName](one, two, three, four); callbackName = name + '3'; if (this[callbackName]) { this[callbackName](one, two, three, four); let counter = 4; while (this[callbackName = name + counter.toString()] !== undefined) { this[callbackName](one, two, three, four); counter++; } } } } } /** * Registers view engine hooks for the view. * @param hooks The hooks to register. */ registerViewEngineHooks(hooks:ViewEngineHooks): void { this._tryAddHook(hooks, 'beforeCompile'); this._tryAddHook(hooks, 'afterCompile'); this._tryAddHook(hooks, 'beforeCreate'); this._tryAddHook(hooks, 'afterCreate'); this._tryAddHook(hooks, 'beforeBind'); this._tryAddHook(hooks, 'beforeUnbind'); } /** * Gets the binding language associated with these resources, or return the provided fallback implementation. * @param bindingLanguageFallback The fallback binding language implementation to use if no binding language is configured locally. * @return The binding language. */ getBindingLanguage(bindingLanguageFallback: BindingLanguage): BindingLanguage { return this.bindingLanguage || (this.bindingLanguage = bindingLanguageFallback); } /** * Patches an immediate parent into the view resource resolution hierarchy. * @param newParent The new parent resources to patch in. */ patchInParent(newParent: ViewResources): void { let originalParent = this.parent; this.parent = newParent || null; this.hasParent = this.parent !== null; if (newParent.parent === null) { newParent.parent = originalParent; newParent.hasParent = originalParent !== null; } } /** * Maps a path relative to the associated view's origin. * @param path The relative path. * @return The calcualted path. */ relativeToView(path: string): string { return relativeToFile(path, this.viewUrl); } /** * Registers an HTML element. * @param tagName The name of the custom element. * @param behavior The behavior of the element. */ registerElement(tagName: string, behavior: HtmlBehaviorResource): void { register(this.elements, tagName, behavior, 'an Element'); } /** * Gets an HTML element behavior. * @param tagName The tag name to search for. * @return The HtmlBehaviorResource for the tag name or null. */ getElement(tagName: string): HtmlBehaviorResource { return this.elements[tagName] || (this.hasParent ? this.parent.getElement(tagName) : null); } /** * Gets the known attribute name based on the local attribute name. * @param attribute The local attribute name to lookup. * @return The known name. */ mapAttribute(attribute: string): string { return this.attributeMap[attribute] || (this.hasParent ? this.parent.mapAttribute(attribute) : null); } /** * Registers an HTML attribute. * @param attribute The name of the attribute. * @param behavior The behavior of the attribute. * @param knownAttribute The well-known name of the attribute (in lieu of the local name). */ registerAttribute(attribute: string, behavior: HtmlBehaviorResource, knownAttribute: string): void { this.attributeMap[attribute] = knownAttribute; register(this.attributes, attribute, behavior, 'an Attribute'); } /** * Gets an HTML attribute behavior. * @param attribute The name of the attribute to lookup. * @return The HtmlBehaviorResource for the attribute or null. */ getAttribute(attribute: string): HtmlBehaviorResource { return this.attributes[attribute] || (this.hasParent ? this.parent.getAttribute(attribute) : null); } /** * Registers a value converter. * @param name The name of the value converter. * @param valueConverter The value converter instance. */ registerValueConverter(name: string, valueConverter: Object): void { register(this.valueConverters, name, valueConverter, 'a ValueConverter'); } /** * Gets a value converter. * @param name The name of the value converter. * @return The value converter instance. */ getValueConverter(name: string): Object { return this.valueConverters[name] || (this.hasParent ? this.parent.getValueConverter(name) : null); } /** * Registers a binding behavior. * @param name The name of the binding behavior. * @param bindingBehavior The binding behavior instance. */ registerBindingBehavior(name: string, bindingBehavior: Object): void { register(this.bindingBehaviors, name, bindingBehavior, 'a BindingBehavior'); } /** * Gets a binding behavior. * @param name The name of the binding behavior. * @return The binding behavior instance. */ getBindingBehavior(name: string): Object { return this.bindingBehaviors[name] || (this.hasParent ? this.parent.getBindingBehavior(name) : null); } /** * Registers a value. * @param name The name of the value. * @param value The value. */ registerValue(name: string, value: any): void { register(this.values, name, value, 'a value'); } /** * Gets a value. * @param name The name of the value. * @return The value. */ getValue(name: string): any { return this.values[name] || (this.hasParent ? this.parent.getValue(name) : null); } }