UNPKG

@danielkalen/simplybind

Version:

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

216 lines (184 loc) 6.8 kB
import * as LogManager from 'aurelia-logging'; import {DOM} from 'aurelia-pal'; import {TaskQueue} from 'aurelia-task-queue'; import {getArrayObserver} from './array-observation'; import {getMapObserver} from './map-observation'; import {getSetObserver} from './set-observation'; import {EventManager} from './event-manager'; import {Parser} from './parser'; import {DirtyChecker, DirtyCheckProperty} from './dirty-checking'; import { SetterObserver, PrimitiveObserver, propertyAccessor } from './property-observation'; import {SelectValueObserver} from './select-value-observer'; import {CheckedObserver} from './checked-observer'; import { ValueAttributeObserver, XLinkAttributeObserver, DataAttributeObserver, StyleObserver, dataAttributeAccessor } from './element-observation'; import {ClassObserver} from './class-observer'; import { hasDeclaredDependencies, createComputedObserver } from './computed-observation'; import {SVGAnalyzer} from './svg'; export class ObserverLocator { static inject = [TaskQueue, EventManager, DirtyChecker, SVGAnalyzer, Parser]; constructor(taskQueue, eventManager, dirtyChecker, svgAnalyzer, parser) { this.taskQueue = taskQueue; this.eventManager = eventManager; this.dirtyChecker = dirtyChecker; this.svgAnalyzer = svgAnalyzer; this.parser = parser; this.adapters = []; this.logger = LogManager.getLogger('observer-locator'); } getObserver(obj, propertyName) { let observersLookup = obj.__observers__; let observer; if (observersLookup && propertyName in observersLookup) { return observersLookup[propertyName]; } observer = this.createPropertyObserver(obj, propertyName); if (!observer.doNotCache) { if (observersLookup === undefined) { observersLookup = this.getOrCreateObserversLookup(obj); } observersLookup[propertyName] = observer; } return observer; } getOrCreateObserversLookup(obj) { return obj.__observers__ || this.createObserversLookup(obj); } createObserversLookup(obj) { let value = {}; if (!Reflect.defineProperty(obj, '__observers__', { enumerable: false, configurable: false, writable: false, value: value })) { this.logger.warn('Cannot add observers to object', obj); } return value; } addAdapter(adapter: ObjectObservationAdapter) { this.adapters.push(adapter); } getAdapterObserver(obj, propertyName, descriptor) { for (let i = 0, ii = this.adapters.length; i < ii; i++) { let adapter = this.adapters[i]; let observer = adapter.getObserver(obj, propertyName, descriptor); if (observer) { return observer; } } return null; } createPropertyObserver(obj, propertyName) { let descriptor; let handler; let xlinkResult; if (!(obj instanceof Object)) { return new PrimitiveObserver(obj, propertyName); } if (obj instanceof DOM.Element) { if (propertyName === 'class') { return new ClassObserver(obj); } if (propertyName === 'style' || propertyName === 'css') { return new StyleObserver(obj, propertyName); } handler = this.eventManager.getElementHandler(obj, propertyName); if (propertyName === 'value' && obj.tagName.toLowerCase() === 'select') { return new SelectValueObserver(obj, handler, this); } if (propertyName === 'checked' && obj.tagName.toLowerCase() === 'input') { return new CheckedObserver(obj, handler, this); } if (handler) { return new ValueAttributeObserver(obj, propertyName, handler); } xlinkResult = /^xlink:(.+)$/.exec(propertyName); if (xlinkResult) { return new XLinkAttributeObserver(obj, propertyName, xlinkResult[1]); } if (/^\w+:|^data-|^aria-/.test(propertyName) || obj instanceof DOM.SVGElement && this.svgAnalyzer.isStandardSvgAttribute(obj.nodeName, propertyName)) { return new DataAttributeObserver(obj, propertyName); } } descriptor = Object.getPropertyDescriptor(obj, propertyName); if (hasDeclaredDependencies(descriptor)) { return createComputedObserver(obj, propertyName, descriptor, this); } if (descriptor) { const existingGetterOrSetter = descriptor.get || descriptor.set; if (existingGetterOrSetter) { if (existingGetterOrSetter.getObserver) { return existingGetterOrSetter.getObserver(obj); } // attempt to use an adapter before resorting to dirty checking. let adapterObserver = this.getAdapterObserver(obj, propertyName, descriptor); if (adapterObserver) { return adapterObserver; } return new DirtyCheckProperty(this.dirtyChecker, obj, propertyName); } } if (obj instanceof Array) { if (propertyName === 'length') { return this.getArrayObserver(obj).getLengthObserver(); } return new DirtyCheckProperty(this.dirtyChecker, obj, propertyName); } else if (obj instanceof Map) { if (propertyName === 'size') { return this.getMapObserver(obj).getLengthObserver(); } return new DirtyCheckProperty(this.dirtyChecker, obj, propertyName); } else if (obj instanceof Set) { if (propertyName === 'size') { return this.getSetObserver(obj).getLengthObserver(); } return new DirtyCheckProperty(this.dirtyChecker, obj, propertyName); } return new SetterObserver(this.taskQueue, obj, propertyName); } getAccessor(obj, propertyName) { if (obj instanceof DOM.Element) { if (propertyName === 'class' || propertyName === 'style' || propertyName === 'css' || propertyName === 'value' && (obj.tagName.toLowerCase() === 'input' || obj.tagName.toLowerCase() === 'select') || propertyName === 'checked' && obj.tagName.toLowerCase() === 'input' || propertyName === 'model' && obj.tagName.toLowerCase() === 'input' || /^xlink:.+$/.exec(propertyName)) { return this.getObserver(obj, propertyName); } if (/^\w+:|^data-|^aria-/.test(propertyName) || obj instanceof DOM.SVGElement && this.svgAnalyzer.isStandardSvgAttribute(obj.nodeName, propertyName)) { return dataAttributeAccessor; } } return propertyAccessor; } getArrayObserver(array) { return getArrayObserver(this.taskQueue, array); } getMapObserver(map) { return getMapObserver(this.taskQueue, map); } getSetObserver(set) { return getSetObserver(this.taskQueue, set); } } export class ObjectObservationAdapter { getObserver(object, propertyName, descriptor) { throw new Error('BindingAdapters must implement getObserver(object, propertyName).'); } }