aurelia-typed-observable-plugin
Version:
A plugin for @observable and @bindable value coercion
189 lines (165 loc) • 6.3 kB
text/typescript
// tslint:disable: interface-name no-invalid-this no-non-null-assertion
import * as LogManager from 'aurelia-logging';
import { TaskQueue } from 'aurelia-task-queue';
import { BehaviorPropertyObserver, BindableProperty, HtmlBehaviorResource } from 'aurelia-templating';
import { coerceFunctions } from './coerce-functions';
import { ICoerceFunction } from './interfaces';
/** @internal */
declare module 'aurelia-templating' {
interface BehaviorPropertyObserver {
coerce?: ICoerceFunction;
// currentValue: any;
// oldValue: any;
// selfSubscriber: Function | null;
setCoerce(coerce: string | Function | undefined): void;
}
interface BindableProperty {
name: any;
coerce: string | ICoerceFunction | undefined;
}
}
interface ExtendedBehaviorPropertyObserver extends BehaviorPropertyObserver {
// coerce?: ICoerceFunction;
currentValue: any;
oldValue: any;
selfSubscriber: Function | null;
notqueued: boolean;
publishing: boolean;
taskQueue: TaskQueue;
// setCoerce(coerce: string | Function | undefined): void;
}
interface BindablePropertyForeignExtension {
changeHandler: string;
defaultValue: any;
owner: HtmlBehaviorResource & { taskQueue: TaskQueue };
}
interface ExtendedBindableProperty extends BindableProperty {
changeHandler: string;
defaultValue: any;
owner: HtmlBehaviorResource & { taskQueue: TaskQueue };
}
BehaviorPropertyObserver.prototype.setCoerce = function(coerce: string | ICoerceFunction): void {
this.coerce = typeof coerce === 'function' ? coerce : coerceFunctions[coerce];
if (this.coerce === undefined) {
LogManager
.getLogger('behavior-property-observer')
.warn(`Invalid coerce instruction. Should be either one of ${Object.keys(coerceFunctions)} or a function.`);
}
};
/**
* Slightly override the builtin implementation that will handle coercion
*/
BehaviorPropertyObserver.prototype.setValue = function(
this: BehaviorPropertyObserver & ExtendedBehaviorPropertyObserver,
newValue: any
): void {
const oldValue = this.currentValue;
const coercedValue = this.coerce === undefined ? newValue : this.coerce(newValue);
if (oldValue !== coercedValue) {
this.oldValue = oldValue;
this.currentValue = coercedValue;
if (this.publishing && this.notqueued) {
if (this.taskQueue.flushing) {
this.call();
} else {
this.notqueued = false;
this.taskQueue.queueMicroTask(this);
}
}
}
};
BindableProperty.prototype.createObserver = function(
this: ExtendedBindableProperty,
viewModel: any
): BehaviorPropertyObserver {
let selfSubscriber: Function | null = null;
const defaultValue = this.defaultValue;
const changeHandlerName = this.changeHandler;
const name = this.name;
let initialValue;
if ((this as any).hasOptions) {
return undefined;
}
if (changeHandlerName in viewModel) {
if ('propertyChanged' in viewModel) {
selfSubscriber = (newValue: any, oldValue: any) => {
viewModel[changeHandlerName](newValue, oldValue);
viewModel.propertyChanged(name, newValue, oldValue);
};
} else {
selfSubscriber = (newValue: any, oldValue: any) => viewModel[changeHandlerName](newValue, oldValue);
}
} else if ('propertyChanged' in viewModel) {
selfSubscriber = (newValue: any, oldValue: any) => viewModel.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;
}
const observer: ExtendedBehaviorPropertyObserver = new BehaviorPropertyObserver(
this.owner.taskQueue,
viewModel,
this.name,
selfSubscriber,
initialValue
) as any;
if (this.coerce !== undefined) {
observer.setCoerce(this.coerce);
observer.currentValue = observer.oldValue = observer.coerce === undefined ? observer.currentValue : observer.coerce(initialValue);
}
return observer;
};
(BindableProperty as any).prototype._createDynamicProperty = function(
this: BindableProperty & BindablePropertyForeignExtension,
viewModel: any,
observerLookup: Record<string, ExtendedBehaviorPropertyObserver>,
behaviorHandlesBind: boolean,
name: string,
attribute: string | { createBinding(viewModel: any): any },
boundProperties: any[]
): void {
const changeHandlerName = `${name}Changed`;
let selfSubscriber: Function | null = null;
let observer: ExtendedBehaviorPropertyObserver;
let info;
if (changeHandlerName in viewModel) {
if ('propertyChanged' in viewModel) {
selfSubscriber = (newValue: any, oldValue: any) => {
viewModel[changeHandlerName](newValue, oldValue);
viewModel.propertyChanged(name, newValue, oldValue);
};
} else {
selfSubscriber = (newValue: any, oldValue: any) => viewModel[changeHandlerName](newValue, oldValue);
}
} else if ('propertyChanged' in viewModel) {
selfSubscriber = (newValue: any, oldValue: any) => viewModel.propertyChanged(name, newValue, oldValue);
}
observer = observerLookup[name] = new BehaviorPropertyObserver(
this.owner.taskQueue,
viewModel,
name,
selfSubscriber,
undefined
) as ExtendedBehaviorPropertyObserver;
observer.setCoerce(this.coerce);
observer.currentValue = observer.oldValue = observer.coerce === undefined ? observer.currentValue : observer.coerce(undefined);
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;
};