@nativescript/core
Version:
A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.
535 lines • 24.3 kB
JavaScript
// Requires
import { unsetValue } from '../properties';
import { Observable } from '../../../data/observable';
import { addWeakEventListener, removeWeakEventListener } from '../weak-event-listener';
import { bindingConstants, parentsRegex } from '../../builder/binding-builder';
import { escapeRegexSymbols } from '../../../utils';
import { Trace } from '../../../trace';
import { parseExpression, convertExpressionToValue } from './bindable-expressions';
import * as types from '../../../utils/types';
import * as bindableResources from './bindable-resources';
const contextKey = 'context';
// this regex is used to get parameters inside [] for example:
// from $parents['ListView'] will return 'ListView'
// from $parents[1] will return 1
const paramsRegex = /\[\s*(['"])*(\w*)\1\s*\]/;
const bc = bindingConstants;
const emptyArray = [];
const propertiesCache = {};
function getProperties(property) {
if (!property) {
return emptyArray;
}
let result = propertiesCache[property];
if (result) {
return result;
}
// first replace all '$parents[..]' with a safe string
// second removes all ] since they are not important for property access and not needed
// then split properties either on '.' or '['
const parentsMatches = property.match(parentsRegex);
result = property.replace(parentsRegex, 'parentsMatch').replace(/\]/g, '').split(/\.|\[/);
let parentsMatchesCounter = 0;
for (let i = 0, resultLength = result.length; i < resultLength; i++) {
if (result[i] === 'parentsMatch') {
result[i] = parentsMatches[parentsMatchesCounter++];
}
}
propertiesCache[property] = result;
return result;
}
export function getEventOrGestureName(name) {
return name.indexOf('on') === 0 ? name.substr(2, name.length - 2) : name;
}
// NOTE: method fromString from "ui/gestures";
export function isGesture(eventOrGestureName) {
const t = eventOrGestureName.trim().toLowerCase();
return t === 'tap' || t === 'doubletap' || t === 'pinch' || t === 'pan' || t === 'swipe' || t === 'rotation' || t === 'longpress' || t === 'touch';
}
// TODO: Make this instance function so that we dont need public statc tapEvent = "tap"
// in controls. They will just override this one and provide their own event support.
export function isEventOrGesture(name, view) {
if (typeof name === 'string') {
const eventOrGestureName = getEventOrGestureName(name);
const evt = `${eventOrGestureName}Event`;
return (view.constructor && evt in view.constructor) || isGesture(eventOrGestureName.toLowerCase());
}
return false;
}
export class Binding {
constructor(target, options) {
this.propertyChangeListeners = new Map();
this.target = new WeakRef(target);
this.options = options;
this.sourceProperties = getProperties(options.sourceProperty);
this.targetOptions = this.resolveOptions(target, getProperties(options.targetProperty));
if (!this.targetOptions) {
throw new Error(`Invalid property: ${options.targetProperty} for target: ${target}`);
}
if (options.twoWay) {
const target = this.targetOptions.instance.get();
if (target instanceof Observable) {
target.on(`${this.targetOptions.property}Change`, this.onTargetPropertyChanged, this);
}
}
}
onTargetPropertyChanged(data) {
this.updateTwoWay(data.value);
}
loadedHandlerVisualTreeBinding(args) {
const target = args.object;
target.off('loaded', this.loadedHandlerVisualTreeBinding, this);
const context = target.bindingContext;
if (context !== undefined && context !== null) {
this.update(context);
}
}
clearSource() {
this.propertyChangeListeners.forEach((observable, index, map) => {
removeWeakEventListener(observable, Observable.propertyChangeEvent, this.onSourcePropertyChanged, this);
});
this.propertyChangeListeners.clear();
// if (this.source) {
// this.source.clear();
// }
if (this.sourceOptions) {
this.sourceOptions.instance = undefined;
this.sourceOptions = undefined;
}
}
sourceAsObject(source) {
/* tslint:disable */
const objectType = typeof source;
if (objectType === 'number') {
source = new Number(source);
}
else if (objectType === 'boolean') {
source = new Boolean(source);
}
else if (objectType === 'string') {
source = new String(source);
}
/* tslint:enable */
return source;
}
bindingContextChanged(data) {
const target = this.targetOptions.instance.get();
if (!target) {
this.unbind();
return;
}
const value = data.value;
if (value !== null && value !== undefined) {
this.update(value);
}
else {
// TODO: Is this correct?
// What should happen when bindingContext is null/undefined?
this.clearBinding();
}
// TODO: if oneWay - call target.unbind();
}
bind(source) {
const target = this.targetOptions.instance.get();
if (this.sourceIsBindingContext && target instanceof Observable && this.targetOptions.property !== 'bindingContext') {
target.on('bindingContextChange', this.bindingContextChanged, this);
}
this.update(source);
}
update(source) {
this.clearSource();
source = this.sourceAsObject(source);
if (!types.isNullOrUndefined(source)) {
// TODO: if oneWay - call target.unbind();
this.source = new WeakRef(source);
this.sourceOptions = this.resolveOptions(source, this.sourceProperties);
const sourceValue = this.getSourcePropertyValue();
this.updateTarget(sourceValue);
this.addPropertyChangeListeners(this.source, this.sourceProperties);
}
else if (!this.sourceIsBindingContext) {
// TODO: if oneWay - call target.unbind();
const sourceValue = this.getSourcePropertyValue();
this.updateTarget(sourceValue ? sourceValue : source);
}
}
unbind() {
const target = this.targetOptions.instance.get();
if (target instanceof Observable) {
if (this.options.twoWay) {
target.off(`${this.targetOptions.property}Change`, this.onTargetPropertyChanged, this);
}
if (this.sourceIsBindingContext && this.targetOptions.property !== 'bindingContext') {
target.off('bindingContextChange', this.bindingContextChanged, this);
}
}
if (this.targetOptions) {
this.targetOptions = undefined;
}
this.sourceProperties = undefined;
if (!this.source) {
return;
}
this.clearSource();
}
// Consider returning single {} instead of array for performance.
resolveObjectsAndProperties(source, properties) {
const result = [];
let currentObject = source;
let currentObjectChanged = false;
for (let i = 0, propsArrayLength = properties.length; i < propsArrayLength; i++) {
const property = properties[i];
if (property === bc.bindingValueKey) {
currentObjectChanged = true;
}
if (property === bc.parentValueKey || property.indexOf(bc.parentsValueKey) === 0) {
const parentView = this.getParentView(this.target.get(), property).view;
if (parentView) {
currentObject = parentView.bindingContext;
}
else {
const targetInstance = this.target.get();
targetInstance.off('loaded', this.loadedHandlerVisualTreeBinding, this);
targetInstance.on('loaded', this.loadedHandlerVisualTreeBinding, this);
}
currentObjectChanged = true;
}
if (currentObject) {
result.push({ instance: currentObject, property: property });
}
else {
break;
}
// do not need to dive into last object property getter on binding stage will handle it
if (!currentObjectChanged && i < propsArrayLength - 1) {
currentObject = currentObject ? currentObject[properties[i]] : null;
}
currentObjectChanged = false;
}
return result;
}
addPropertyChangeListeners(source, sourceProperty, parentProperies) {
const objectsAndProperties = this.resolveObjectsAndProperties(source.get(), sourceProperty);
let prop = parentProperies || '';
for (let i = 0, length = objectsAndProperties.length; i < length; i++) {
const propName = objectsAndProperties[i].property;
prop += '$' + propName;
const currentObject = objectsAndProperties[i].instance;
if (!this.propertyChangeListeners.has(prop) && currentObject instanceof Observable && currentObject._isViewBase) {
// Add listener for properties created with after 3.0 version
addWeakEventListener(currentObject, `${propName}Change`, this.onSourcePropertyChanged, this);
addWeakEventListener(currentObject, Observable.propertyChangeEvent, this.onSourcePropertyChanged, this);
this.propertyChangeListeners.set(prop, currentObject);
}
else if (!this.propertyChangeListeners.has(prop) && currentObject instanceof Observable) {
addWeakEventListener(currentObject, Observable.propertyChangeEvent, this.onSourcePropertyChanged, this);
this.propertyChangeListeners.set(prop, currentObject);
}
}
}
prepareExpressionForUpdate() {
// this regex is used to create a valid RegExp object from a string that has some special regex symbols like [,(,$ and so on.
// Basically this method replaces all matches of 'source property' in expression with '$newPropertyValue'.
// For example: with an expression similar to:
// text="{{ sourceProperty = $parents['ListView'].test, expression = $parents['ListView'].test + 2}}"
// update expression will be '$newPropertyValue + 2'
// then on expression execution the new value will be taken and target property will be updated with the value of the expression.
const escapedSourceProperty = escapeRegexSymbols(this.options.sourceProperty);
const expRegex = new RegExp(escapedSourceProperty, 'g');
const resultExp = this.options.expression.replace(expRegex, bc.newPropertyValueKey);
return resultExp;
}
updateTwoWay(value) {
if (this.updating || !this.options.twoWay) {
return;
}
let newValue = value;
if (__UI_USE_EXTERNAL_RENDERER__) {
}
else if (this.options.expression) {
const changedModel = {};
const targetInstance = this.target.get();
let sourcePropertyName = '';
if (this.sourceOptions) {
sourcePropertyName = this.sourceOptions.property;
}
else if (typeof this.options.sourceProperty === 'string' && this.options.sourceProperty.indexOf('.') === -1) {
sourcePropertyName = this.options.sourceProperty;
}
const updateExpression = this.prepareExpressionForUpdate();
this.prepareContextForExpression(targetInstance, changedModel, updateExpression);
/**
* Wait for 'prepareContextForExpression' to assign keys first and override any possible occurences.
* For example, 'bindingValueKey' key can result in a circular reference if it's set in both cases.
*/
changedModel[bc.bindingValueKey] = value;
changedModel[bc.newPropertyValueKey] = value;
if (sourcePropertyName !== '') {
changedModel[sourcePropertyName] = value;
}
const expressionValue = this._getExpressionValue(updateExpression, true, changedModel);
if (expressionValue instanceof Error) {
Trace.write(expressionValue.message, Trace.categories.Binding, Trace.messageType.error);
}
newValue = expressionValue;
}
this.updateSource(newValue);
}
_getExpressionValue(expression, isBackConvert, changedModel) {
let result = null;
if (!__UI_USE_EXTERNAL_RENDERER__) {
let context;
const targetInstance = this.target.get();
const addedProps = [];
try {
let exp;
try {
exp = parseExpression(expression);
}
catch (e) {
result = new Error(e + ' at ' + targetInstance);
}
if (exp) {
context = (this.source && this.source.get && this.source.get()) || global;
const resources = bindableResources.get();
for (const prop in resources) {
if (resources.hasOwnProperty(prop) && !context.hasOwnProperty(prop)) {
context[prop] = resources[prop];
addedProps.push(prop);
}
}
// For expressions, there are also cases when binding must be updated after component is loaded (e.g. ListView)
if (this.prepareContextForExpression(targetInstance, context, expression, addedProps)) {
result = convertExpressionToValue(exp, context, isBackConvert, changedModel ? changedModel : context);
}
else {
targetInstance.off('loaded', this.loadedHandlerVisualTreeBinding, this);
targetInstance.on('loaded', this.loadedHandlerVisualTreeBinding, this);
}
}
}
catch (e) {
result = new Error(e + ' at ' + targetInstance);
}
// Clear added props
for (const prop of addedProps) {
delete context[prop];
}
addedProps.length = 0;
}
return result;
}
onSourcePropertyChanged(data) {
const sourceProps = this.sourceProperties;
const sourcePropsLength = sourceProps.length;
let changedPropertyIndex = sourceProps.indexOf(data.propertyName);
let parentProps = '';
if (changedPropertyIndex > -1) {
parentProps = '$' + sourceProps.slice(0, changedPropertyIndex + 1).join('$');
while (this.propertyChangeListeners.get(parentProps) !== data.object) {
changedPropertyIndex += sourceProps.slice(changedPropertyIndex + 1).indexOf(data.propertyName) + 1;
parentProps = '$' + sourceProps.slice(0, changedPropertyIndex + 1).join('$');
}
}
if (__UI_USE_EXTERNAL_RENDERER__ || !this.options.expression) {
if (changedPropertyIndex > -1) {
const props = sourceProps.slice(changedPropertyIndex + 1);
const propsLength = props.length;
if (propsLength > 0) {
let value = data.value;
for (let i = 0; i < propsLength; i++) {
value = value[props[i]];
}
this.updateTarget(value);
}
else if (data.propertyName === this.sourceOptions.property) {
this.updateTarget(data.value);
}
}
}
else {
const expressionValue = this._getExpressionValue(this.options.expression, false, undefined);
if (expressionValue instanceof Error) {
Trace.write(expressionValue.message, Trace.categories.Binding, Trace.messageType.error);
}
else {
this.updateTarget(expressionValue);
}
}
// we need to do this only if nested objects are used as source and some middle object has changed.
if (changedPropertyIndex > -1 && changedPropertyIndex < sourcePropsLength - 1) {
const probablyChangedObject = this.propertyChangeListeners.get(parentProps);
if (probablyChangedObject && probablyChangedObject !== data.object[sourceProps[changedPropertyIndex]]) {
// remove all weakevent listeners after change, because changed object replaces object that is hooked for
// propertyChange event
for (let i = sourcePropsLength - 1; i > changedPropertyIndex; i--) {
const prop = '$' + sourceProps.slice(0, i + 1).join('$');
if (this.propertyChangeListeners.has(prop)) {
removeWeakEventListener(this.propertyChangeListeners.get(prop), Observable.propertyChangeEvent, this.onSourcePropertyChanged, this);
this.propertyChangeListeners.delete(prop);
}
}
const newProps = sourceProps.slice(changedPropertyIndex + 1);
// add new weak event listeners
const newObject = data.object[sourceProps[changedPropertyIndex]];
if (!types.isNullOrUndefined(newObject) && typeof newObject === 'object') {
this.addPropertyChangeListeners(new WeakRef(newObject), newProps, parentProps);
}
}
}
}
prepareContextForExpression(target, model, expression, addedProps = []) {
let success = true;
let parentViewAndIndex;
let parentView;
let expressionCP = expression;
if (expressionCP.indexOf(bc.bindingValueKey) > -1) {
model[bc.bindingValueKey] = model;
addedProps.push(bc.bindingValueKey);
}
const parentsArray = expressionCP.match(parentsRegex);
if (parentsArray) {
for (let i = 0; i < parentsArray.length; i++) {
// This prevents later checks to mistake $parents[] for $parent
expressionCP = expressionCP.replace(parentsArray[i], '');
parentViewAndIndex = this.getParentView(target, parentsArray[i]);
if (parentViewAndIndex.view) {
model[bc.parentsValueKey] = model[bc.parentsValueKey] || {};
model[bc.parentsValueKey][parentViewAndIndex.index] = parentViewAndIndex.view.bindingContext;
addedProps.push(bc.parentsValueKey);
}
else {
success = false;
}
}
}
if (expressionCP.indexOf(bc.parentValueKey) > -1) {
parentView = this.getParentView(target, bc.parentValueKey).view;
if (parentView) {
model[bc.parentValueKey] = parentView.bindingContext;
addedProps.push(bc.parentValueKey);
}
else {
success = false;
}
}
return success;
}
getSourcePropertyValue() {
if (__UI_USE_EXTERNAL_RENDERER__) {
}
else if (this.options.expression) {
const changedModel = {};
changedModel[bc.bindingValueKey] = this.source ? this.source.get() : undefined;
const expressionValue = this._getExpressionValue(this.options.expression, false, changedModel);
if (expressionValue instanceof Error) {
Trace.write(expressionValue.message, Trace.categories.Binding, Trace.messageType.error);
}
else {
return expressionValue;
}
}
if (this.sourceOptions) {
const sourceOptionsInstance = this.sourceOptions.instance.get();
if (this.sourceOptions.property === bc.bindingValueKey) {
return sourceOptionsInstance;
}
else if (sourceOptionsInstance instanceof Observable && this.sourceOptions.property && this.sourceOptions.property !== '') {
return sourceOptionsInstance.get(this.sourceOptions.property);
}
else if (sourceOptionsInstance && this.sourceOptions.property && this.sourceOptions.property !== '' && this.sourceOptions.property in sourceOptionsInstance) {
return sourceOptionsInstance[this.sourceOptions.property];
}
else {
Trace.write("Property: '" + this.sourceOptions.property + "' is invalid or does not exist. SourceProperty: '" + this.options.sourceProperty + "'", Trace.categories.Binding, Trace.messageType.error);
}
}
return null;
}
clearBinding() {
this.clearSource();
this.updateTarget(unsetValue);
}
updateTarget(value) {
if (this.updating) {
return;
}
this.updateOptions(this.targetOptions, types.isNullOrUndefined(value) ? unsetValue : value);
}
updateSource(value) {
if (this.updating || !this.source || !this.source.get()) {
return;
}
this.updateOptions(this.sourceOptions, value);
}
getParentView(target, property) {
if (!target) {
return { view: null, index: null };
}
let result;
if (property === bc.parentValueKey) {
result = target.parent;
}
let index = null;
if (property.indexOf(bc.parentsValueKey) === 0) {
result = target.parent;
const indexParams = paramsRegex.exec(property);
if (indexParams && indexParams.length > 1) {
index = indexParams[2];
}
if (!isNaN(index)) {
let indexAsInt = parseInt(index);
while (indexAsInt > 0) {
result = result.parent;
indexAsInt--;
}
}
else if (types.isString(index)) {
while (result && result.typeName !== index) {
result = result.parent;
}
}
}
return { view: result, index: index };
}
resolveOptions(obj, properties) {
const objectsAndProperties = this.resolveObjectsAndProperties(obj, properties);
if (objectsAndProperties.length > 0) {
const resolvedObj = objectsAndProperties[objectsAndProperties.length - 1].instance;
const prop = objectsAndProperties[objectsAndProperties.length - 1].property;
return {
instance: new WeakRef(this.sourceAsObject(resolvedObj)),
property: prop,
};
}
return null;
}
updateOptions(options, value) {
let optionsInstance;
if (options && options.instance) {
optionsInstance = options.instance.get();
}
if (!optionsInstance) {
return;
}
this.updating = true;
try {
if (isEventOrGesture(options.property, optionsInstance) && types.isFunction(value)) {
// calling off method with null as handler will remove all handlers for options.property event
optionsInstance.off(options.property, null, optionsInstance.bindingContext);
optionsInstance.on(options.property, value, optionsInstance.bindingContext);
}
else if (optionsInstance instanceof Observable) {
optionsInstance.set(options.property, value);
}
else {
optionsInstance[options.property] = value;
}
}
catch (ex) {
Trace.write('Binding error while setting property ' + options.property + ' of ' + optionsInstance + ': ' + ex, Trace.categories.Binding, Trace.messageType.error);
}
this.updating = false;
}
}
//# sourceMappingURL=index.js.map