aurelia-templating-binding
Version:
An implementation of the templating engine's Binding Language abstraction which uses a pluggable command syntax.
333 lines (295 loc) • 9.14 kB
text/typescript
import { Binding, bindingMode, connectable, enqueueBindingConnect, Expression, LookupFunctions, ObserverLocator, Scope } from 'aurelia-binding';
import * as LogManager from 'aurelia-logging';
export class InterpolationBindingExpression {
/** @internal */
private observerLocator: ObserverLocator;
/** @internal */
private targetProperty: string;
/** @internal */
parts: (string | Expression)[];
/** @internal */
mode: bindingMode;
/** @internal */
private lookupFunctions: LookupFunctions;
/** @internal */
private attribute: string;
/** @internal */
attrToRemove: string;
/** @internal */
discrete: boolean;
constructor(
observerLocator: ObserverLocator,
targetProperty: string,
parts: (string | Expression)[],
mode: bindingMode,
lookupFunctions: LookupFunctions,
attribute: string
) {
this.observerLocator = observerLocator;
this.targetProperty = targetProperty;
this.parts = parts;
this.mode = mode;
this.lookupFunctions = lookupFunctions;
this.attribute = this.attrToRemove = attribute;
this.discrete = false;
}
createBinding(target) {
if (this.parts.length === 3) {
return new ChildInterpolationBinding(
target,
this.observerLocator,
this.parts[1] as Expression,
this.mode,
this.lookupFunctions,
this.targetProperty,
this.parts[0] as string,
this.parts[2] as string
);
}
return new InterpolationBinding(
this.observerLocator,
this.parts,
target,
this.targetProperty,
this.mode,
this.lookupFunctions
);
}
}
function validateTarget(target, propertyName) {
if (propertyName === 'style') {
LogManager.getLogger('templating-binding')
.info('Internet Explorer does not support interpolation in "style" attributes. Use the style attribute\'s alias, "css" instead.');
} else if (target.parentElement && target.parentElement.nodeName === 'TEXTAREA' && propertyName === 'textContent') {
throw new Error('Interpolation binding cannot be used in the content of a textarea element. Use <textarea value.bind="expression"></textarea> instead.');
}
}
export class InterpolationBinding {
/** @internal */
private observerLocator: ObserverLocator;
/** @internal */
private parts: (string | Expression)[];
/** @internal */
private target: any;
/** @internal */
private targetProperty: string;
/** @internal */
targetAccessor: InternalPropertyAccessor;
/** @internal */
mode: bindingMode;
/** @internal */
private lookupFunctions: LookupFunctions;
/** @internal */
isBound: boolean;
/** @internal */
source: Scope;
constructor(observerLocator: ObserverLocator, parts: (string | Expression)[], target: any, targetProperty: string, mode: bindingMode, lookupFunctions: LookupFunctions) {
validateTarget(target, targetProperty);
this.observerLocator = observerLocator;
this.parts = parts;
this.target = target;
this.targetProperty = targetProperty;
this.targetAccessor = observerLocator.getAccessor(target, targetProperty);
this.mode = mode;
this.lookupFunctions = lookupFunctions;
}
interpolate() {
if (this.isBound) {
let value = '';
let parts = this.parts;
for (let i = 0, ii = parts.length; i < ii; i++) {
value += (i % 2 === 0 ? parts[i] : this[`childBinding${i}`].value);
}
this.targetAccessor.setValue(value, this.target, this.targetProperty);
}
}
updateOneTimeBindings() {
for (let i = 1, ii = this.parts.length; i < ii; i += 2) {
let child = this[`childBinding${i}`];
if (child.mode === bindingMode.oneTime) {
child.call();
}
}
}
bind(source) {
if (this.isBound) {
if (this.source === source) {
return;
}
this.unbind();
}
this.source = source;
let parts = this.parts;
for (let i = 1, ii = parts.length; i < ii; i += 2) {
let binding = new ChildInterpolationBinding(this, this.observerLocator, parts[i] as Expression, this.mode, this.lookupFunctions);
binding.bind(source);
this[`childBinding${i}`] = binding;
}
this.isBound = true;
this.interpolate();
}
unbind() {
if (!this.isBound) {
return;
}
this.isBound = false;
this.source = null;
let parts = this.parts;
for (let i = 1, ii = parts.length; i < ii; i += 2) {
let name = `childBinding${i}`;
this[name].unbind();
}
}
}
/** @internal */
export interface ChildInterpolationBinding extends Binding {
observeArray(arr: any[]): void;
unobserve(all?: boolean): void;
}
export class ChildInterpolationBinding {
/** @internal */
private parent: InterpolationBinding;
/** @internal */
private target: any;
/** @internal */
private targetProperty: string;
/** @internal */
private targetAccessor: InternalPropertyAccessor;
/** @internal */
private observerLocator: ObserverLocator;
/** @internal */
sourceExpression: Expression;
/** @internal */
mode: bindingMode;
/** @internal */
private lookupFunctions: LookupFunctions;
/** @internal */
private left: string;
/** @internal */
private right: string;
/** @internal */
private value: any;
/** @internal */
isBound: boolean;
/** @internal */
private rawValue: any;
/**
* From @connectable decorator
* @internal
*/
private _version: any;
/** @internal */
source: Scope;
constructor(
target: InterpolationBinding,
observerLocator: ObserverLocator,
sourceExpression: Expression,
mode: bindingMode,
lookupFunctions: LookupFunctions,
targetProperty?: string,
left?: string,
right?: string
) {
if (target instanceof InterpolationBinding) {
this.parent = target;
} else {
validateTarget(target, targetProperty);
this.target = target;
this.targetProperty = targetProperty;
this.targetAccessor = observerLocator.getAccessor(target, targetProperty);
}
this.observerLocator = observerLocator;
this.sourceExpression = sourceExpression;
this.mode = mode;
this.lookupFunctions = lookupFunctions;
this.left = left;
this.right = right;
}
updateTarget(value) {
value = value === null || value === undefined ? '' : value.toString();
if (value !== this.value) {
this.value = value;
if (this.parent) {
this.parent.interpolate();
} else {
this.targetAccessor.setValue(this.left + value + this.right, this.target, this.targetProperty);
}
}
}
call() {
if (!this.isBound) {
return;
}
this.rawValue = this.sourceExpression.evaluate(this.source, this.lookupFunctions);
this.updateTarget(this.rawValue);
if (this.mode !== bindingMode.oneTime) {
this._version++;
this.sourceExpression.connect(this, this.source);
if (this.rawValue instanceof Array) {
this.observeArray(this.rawValue);
}
this.unobserve(false);
}
}
bind(source) {
if (this.isBound) {
if (this.source === source) {
return;
}
this.unbind();
}
this.isBound = true;
this.source = source;
let sourceExpression = this.sourceExpression;
if (sourceExpression.bind) {
sourceExpression.bind(this, source, this.lookupFunctions);
}
this.rawValue = sourceExpression.evaluate(source, this.lookupFunctions);
this.updateTarget(this.rawValue);
if (this.mode === bindingMode.oneWay) {
enqueueBindingConnect(this);
}
}
unbind() {
if (!this.isBound) {
return;
}
this.isBound = false;
let sourceExpression = this.sourceExpression;
if (sourceExpression.unbind) {
sourceExpression.unbind(this, this.source);
}
this.source = null;
this.value = null;
this.rawValue = null;
this.unobserve(true);
}
connect(evaluate) {
if (!this.isBound) {
return;
}
if (evaluate) {
this.rawValue = this.sourceExpression.evaluate(this.source, this.lookupFunctions);
this.updateTarget(this.rawValue);
}
this.sourceExpression.connect(this, this.source);
if (this.rawValue instanceof Array) {
this.observeArray(this.rawValue);
}
}
}
interface InternalPropertyAccessor {
setValue(value: any, obj: object, prop: string): void;
}
/** @internal */
declare module 'aurelia-binding' {
export function connectable(...args: unknown[]): (...args: unknown[]) => any;
interface ObserverLocator {
getAccessor(obj: object, prop: string): InternalPropertyAccessor;
}
interface Expression {
bind?(binding: Binding, source: Scope, lookup?: LookupFunctions): void;
unbind?(binding: Binding, source: Scope, lookup?: LookupFunctions): void;
}
}