import {_hyphenate} from './util'; import {BehaviorPropertyObserver} from './behavior-property-observer'; import {Binding, BindingExpression, bindingMode} from 'aurelia-binding'; import {Container} from 'aurelia-dependency-injection'; import {metadata} from 'aurelia-metadata'; import { HtmlBehaviorResource } from './html-behavior'; import { ComponentPropertyChanged, ObservableGetterFunction } from './interfaces'; function getObserver(instance, name) { let lookup = instance.__observers__; if (lookup === undefined) { // We need to lookup the actual behavior for this instance, // as it might be a derived class (and behavior) rather than // the class (and behavior) that declared the property calling getObserver(). // This means we can't capture the behavior in property get/set/getObserver and pass it here. // Note that it's probably for the best, as passing the behavior is an overhead // that is only useful in the very first call of the first property of the instance. let ctor = Object.getPrototypeOf(instance).constructor; // Playing safe here, user could have written to instance.constructor. let behavior = metadata.get(metadata.resource, ctor) as HtmlBehaviorResource; if (!behavior.isInitialized) { behavior.initialize(Container.instance || new Container(), instance.constructor); } lookup = behavior.observerLocator.getOrCreateObserversLookup(instance); behavior._ensurePropertiesDefined(instance, lookup); } return lookup[name]; } /** * Represents a bindable property on a behavior. */ export class BindableProperty { name: any; /** @internal */ attribute: any; /** @internal */ defaultBindingMode: any; /** @internal */ changeHandler: string; /** @internal */ owner: any; /** @internal */ descriptor: any; /** @internal */ defaultValue: any; /** @internal */ hasOptions: any; /** @internal */ isDynamic: any; /** * On a custom attribute, `true` indicates this is the primary bindable prop * @internal */ primaryProperty: boolean; /** * Creates an instance of BindableProperty. * @param nameOrConfig The name of the property or a cofiguration object. */ constructor(nameOrConfig: string | Object) { if (typeof nameOrConfig === 'string') { this.name = nameOrConfig; } else { Object.assign(this, nameOrConfig); } this.attribute = this.attribute || _hyphenate(this.name); let defaultBindingMode = this.defaultBindingMode; if (defaultBindingMode === null || defaultBindingMode === undefined) { this.defaultBindingMode = bindingMode.oneWay; } else if (typeof defaultBindingMode === 'string') { // to avoid import from aurelia this.defaultBindingMode = bindingMode[defaultBindingMode] || bindingMode.oneWay; } this.changeHandler = this.changeHandler || null; this.owner = null; this.descriptor = null; } /** * Registers this bindable property with particular Class and Behavior instance. * @param target The class to register this behavior with. * @param behavior The behavior instance to register this property with. * @param descriptor The property descriptor for this property. */ registerWith(target: Function, behavior: HtmlBehaviorResource, descriptor?: Object): void { behavior.properties.push(this); behavior.attributes[this.attribute] = this; this.owner = behavior; if (descriptor) { this.descriptor = descriptor; return this._configureDescriptor(descriptor as any) as unknown as void; } return undefined; } /** @internal */ _configureDescriptor(descriptor: PropertyDescriptor & { initializer: any }): Object { let name = this.name; descriptor.configurable = true; descriptor.enumerable = true; if ('initializer' in descriptor) { this.defaultValue = descriptor.initializer; delete descriptor.initializer; delete descriptor.writable; } if ('value' in descriptor) { this.defaultValue = descriptor.value; delete descriptor.value; delete descriptor.writable; } descriptor.get = function() { return getObserver(this, name).getValue(); }; descriptor.set = function(value) { getObserver(this, name).setValue(value); }; (descriptor.get as ObservableGetterFunction).getObserver = function(obj) { return getObserver(obj, name); }; return descriptor; } /** * Defines this property on the specified class and behavior. * @param target The class to define the property on. * @param behavior The behavior to define the property on. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars defineOn(target: Function, behavior: HtmlBehaviorResource): void { let name = this.name; let handlerName; if (this.changeHandler === null) { handlerName = name + 'Changed'; if (handlerName in target.prototype) { this.changeHandler = handlerName; } } if (this.descriptor === null) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore todo: weird code here Object.defineProperty(target.prototype, name, this._configureDescriptor({})); } } /** * Creates an observer for this property. * @param viewModel The view model instance on which to create the observer. * @return The property observer. */ createObserver(viewModel: Object): BehaviorPropertyObserver { let selfSubscriber = null; let defaultValue = this.defaultValue; let changeHandlerName = this.changeHandler; let name = this.name; let initialValue; if (this.hasOptions) { return undefined; } if (changeHandlerName in viewModel) { if ('propertyChanged' in viewModel) { selfSubscriber = (newValue, oldValue) => { viewModel[changeHandlerName](newValue, oldValue); (viewModel as ComponentPropertyChanged).propertyChanged(name, newValue, oldValue); }; } else { selfSubscriber = (newValue, oldValue) => viewModel[changeHandlerName](newValue, oldValue); } } else if ('propertyChanged' in viewModel) { selfSubscriber = (newValue, oldValue) => (viewModel as ComponentPropertyChanged).propertyChanged(name, newValue, oldValue); } else if (changeHandlerName !== null) { throw new Error(`Change handler ${changeHandlerName} was specified but not declared on the class.`); } if (defaultValue !== undefined) { initialValue = typeof defaultValue === 'function' ? defaultValue.call(viewModel) : defaultValue; } return new BehaviorPropertyObserver(this.owner.taskQueue, viewModel, this.name, selfSubscriber, initialValue); } /** @internal */ _initialize(viewModel: object, observerLookup: Record, attributes: Record, behaviorHandlesBind?: boolean, boundProperties?: BoundPropertyInfo[]): void { let selfSubscriber: Function; let observer: BehaviorPropertyObserver; let attribute: string | BindingExpression; let defaultValue = this.defaultValue; if (this.isDynamic) { for (let key in attributes) { this._createDynamicProperty(viewModel, observerLookup, behaviorHandlesBind, key, attributes[key], boundProperties); } } else if (!this.hasOptions) { observer = observerLookup[this.name]; if (attributes !== null) { selfSubscriber = observer.selfSubscriber; attribute = attributes[this.attribute]; if (behaviorHandlesBind) { observer.selfSubscriber = null; } if (typeof attribute === 'string') { viewModel[this.name] = attribute; observer.call(); } else if (attribute) { boundProperties.push({observer: observer, binding: attribute.createBinding(viewModel)}); } else if (defaultValue !== undefined) { observer.call(); } observer.selfSubscriber = selfSubscriber; } observer.publishing = true; } } /** @internal */ _createDynamicProperty(viewModel, observerLookup, behaviorHandlesBind: boolean, name: string, attribute: string | BindingExpression, boundProperties: BoundPropertyInfo[]) { let changeHandlerName = name + 'Changed'; let selfSubscriber = null; let observer: BehaviorPropertyObserver; let info: BoundPropertyInfo; if (changeHandlerName in viewModel) { if ('propertyChanged' in viewModel) { selfSubscriber = (newValue, oldValue) => { viewModel[changeHandlerName](newValue, oldValue); viewModel.propertyChanged(name, newValue, oldValue); }; } else { selfSubscriber = (newValue, oldValue) => viewModel[changeHandlerName](newValue, oldValue); } } else if ('propertyChanged' in viewModel) { selfSubscriber = (newValue, oldValue) => viewModel.propertyChanged(name, newValue, oldValue); } observer = observerLookup[name] = new BehaviorPropertyObserver( this.owner.taskQueue, viewModel, name, selfSubscriber ); Object.defineProperty(viewModel, name, { configurable: true, enumerable: true, get: observer.getValue.bind(observer), set: observer.setValue.bind(observer) }); if (behaviorHandlesBind) { observer.selfSubscriber = null; } if (typeof attribute === 'string') { viewModel[name] = attribute; observer.call(); } else if (attribute) { info = {observer: observer, binding: attribute.createBinding(viewModel)}; boundProperties.push(info); } observer.publishing = true; observer.selfSubscriber = selfSubscriber; } } export interface BoundPropertyInfo { observer: BehaviorPropertyObserver; binding: Binding; }