mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-15 11:01:21 +08:00
feat: binding expression parser additions and improvements (#9791)
This commit is contained in:

committed by
Nathan Walker

parent
2efcdf5787
commit
716b831523
@ -8,6 +8,8 @@ interface ASTExpression {
|
|||||||
|
|
||||||
const expressionsCache = {};
|
const expressionsCache = {};
|
||||||
|
|
||||||
|
const FORCED_CHAIN_VALUE = Symbol('forcedChain');
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const unaryOperators = {
|
const unaryOperators = {
|
||||||
'+': (v) => +v,
|
'+': (v) => +v,
|
||||||
@ -60,37 +62,43 @@ const expressionParsers = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const left = convertExpressionToValue(expression.left, model, isBackConvert, changedModel);
|
const left = convertExpressionToValue(expression.left, model, isBackConvert, changedModel);
|
||||||
|
|
||||||
|
if (expression.operator == '|' && expression.right.type == 'CallExpression') {
|
||||||
|
expression.right.requiresConverter = true;
|
||||||
|
}
|
||||||
const right = convertExpressionToValue(expression.right, model, isBackConvert, changedModel);
|
const right = convertExpressionToValue(expression.right, model, isBackConvert, changedModel);
|
||||||
|
|
||||||
if (expression.operator == '|') {
|
if (expression.operator == '|') {
|
||||||
if (right != null && isFunction(right.callback) && right.context != null && right.args != null) {
|
if (expression.right.requiresConverter && right != null) {
|
||||||
right.args.unshift(left);
|
right.args.unshift(left);
|
||||||
return right.callback.apply(right.context, right.args);
|
return right.callback.apply(right.context, right.args);
|
||||||
}
|
}
|
||||||
throw new Error('Invalid converter after ' + expression.operator + ' operator');
|
throw new Error('Invalid converter after ' + expression.operator + ' operator');
|
||||||
}
|
}
|
||||||
|
|
||||||
return binaryOperators[expression.operator](left, right);
|
return binaryOperators[expression.operator](left, right);
|
||||||
},
|
},
|
||||||
'CallExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'CallExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
let object;
|
expression.callee.requiresObjectAndProperty = true;
|
||||||
let property;
|
|
||||||
if (expression.callee.type == 'MemberExpression') {
|
const { object, property } = convertExpressionToValue(expression.callee, model, isBackConvert, changedModel);
|
||||||
object = convertExpressionToValue(expression.callee.object, model, isBackConvert, changedModel);
|
|
||||||
property = expression.callee.computed ? convertExpressionToValue(expression.callee.property, model, isBackConvert, changedModel) : expression.callee.property?.name;
|
let callback;
|
||||||
|
if (object == FORCED_CHAIN_VALUE) {
|
||||||
|
callback = undefined;
|
||||||
} else {
|
} else {
|
||||||
object = getContext(expression.callee.name, model, changedModel);
|
callback = expression.callee.optional ? object?.[property] : object[property];
|
||||||
property = expression.callee?.name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const callback = expression.callee.optional ? object?.[property] : object[property];
|
if ((!expression.optional || expression.requiresConverter) && isNullOrUndefined(callback)) {
|
||||||
if (isNullOrUndefined(callback)) {
|
|
||||||
throw new Error('Cannot perform a call using a null or undefined property');
|
throw new Error('Cannot perform a call using a null or undefined property');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isConverter = isObject(callback) && (isFunction(callback.toModel) || isFunction(callback.toView));
|
if (expression.requiresConverter) {
|
||||||
if (!isFunction(callback) && !isConverter) {
|
if (isFunction(callback)) {
|
||||||
throw new Error('Cannot perform a call using a non-callable property');
|
callback = { toView: callback };
|
||||||
|
} else if (!isObject(callback) || !isFunction(callback.toModel) && !isFunction(callback.toView)) {
|
||||||
|
throw new Error('Invalid converter call');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedArgs = [];
|
const parsedArgs = [];
|
||||||
@ -98,7 +106,11 @@ const expressionParsers = {
|
|||||||
let value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
|
let value = convertExpressionToValue(argument, model, isBackConvert, changedModel);
|
||||||
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
|
argument.type == 'SpreadElement' ? parsedArgs.push(...value) : parsedArgs.push(value);
|
||||||
}
|
}
|
||||||
return isConverter ? getConverter(callback, object, parsedArgs, isBackConvert) : callback.apply(object, parsedArgs);
|
|
||||||
|
if (expression.requiresConverter) {
|
||||||
|
return getConverter(callback, object, parsedArgs, isBackConvert);
|
||||||
|
}
|
||||||
|
return expression.optional ? callback?.apply(object, parsedArgs) : callback.apply(object, parsedArgs);
|
||||||
},
|
},
|
||||||
'ChainExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'ChainExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
return convertExpressionToValue(expression.expression, model, isBackConvert, changedModel);
|
return convertExpressionToValue(expression.expression, model, isBackConvert, changedModel);
|
||||||
@ -109,6 +121,9 @@ const expressionParsers = {
|
|||||||
},
|
},
|
||||||
'Identifier': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'Identifier': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
const context = getContext(expression.name, model, changedModel);
|
const context = getContext(expression.name, model, changedModel);
|
||||||
|
if (expression.requiresObjectAndProperty) {
|
||||||
|
return { object: context, property: expression.name };
|
||||||
|
}
|
||||||
return context[expression.name];
|
return context[expression.name];
|
||||||
},
|
},
|
||||||
'Literal': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'Literal': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
@ -122,8 +137,34 @@ const expressionParsers = {
|
|||||||
return logicalOperators[expression.operator](left, () => convertExpressionToValue(expression.right, model, isBackConvert, changedModel));
|
return logicalOperators[expression.operator](left, () => convertExpressionToValue(expression.right, model, isBackConvert, changedModel));
|
||||||
},
|
},
|
||||||
'MemberExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'MemberExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
|
if (expression.object.type == 'MemberExpression') {
|
||||||
|
expression.object.isChained = true;
|
||||||
|
}
|
||||||
|
|
||||||
const object = convertExpressionToValue(expression.object, model, isBackConvert, changedModel);
|
const object = convertExpressionToValue(expression.object, model, isBackConvert, changedModel);
|
||||||
const property = expression.computed ? convertExpressionToValue(expression.property, model, isBackConvert, changedModel) : expression.property?.name;
|
const property = expression.computed ? convertExpressionToValue(expression.property, model, isBackConvert, changedModel) : expression.property?.name;
|
||||||
|
const propertyInfo = { object, property };
|
||||||
|
|
||||||
|
if (expression.requiresObjectAndProperty) {
|
||||||
|
return propertyInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If first member is undefined, make sure that no error is thrown later but return undefined instead.
|
||||||
|
* This behaviour is kept in order to cope with components whose binding context takes a bit long to load.
|
||||||
|
* Old parser would return undefined for an expression like 'property1.property2.property3'
|
||||||
|
* even if expression as a whole consisted of undefined properties.
|
||||||
|
* The new one will keep the same principle only if first member is undefined for safety reasons.
|
||||||
|
* It meddles with members specifically, so that it will not affect expression result as a whole.
|
||||||
|
* For example, an 'isLoading || isBusy' expression will be validated as 'undefined || undefined'
|
||||||
|
* if context is not ready.
|
||||||
|
*/
|
||||||
|
if (object === undefined && expression.object.type == 'Identifier') {
|
||||||
|
return expression.isChained ? FORCED_CHAIN_VALUE : object;
|
||||||
|
}
|
||||||
|
if (object == FORCED_CHAIN_VALUE) {
|
||||||
|
return expression.isChained ? object : undefined;
|
||||||
|
}
|
||||||
return expression.optional ? object?.[property] : object[property];
|
return expression.optional ? object?.[property] : object[property];
|
||||||
},
|
},
|
||||||
'NewExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
'NewExpression': (expression: ASTExpression, model, isBackConvert: boolean, changedModel) => {
|
||||||
@ -211,5 +252,8 @@ export function parseExpression(expressionText: string): ASTExpression {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function convertExpressionToValue(expression: ASTExpression, model, isBackConvert: boolean, changedModel) {
|
export function convertExpressionToValue(expression: ASTExpression, model, isBackConvert: boolean, changedModel) {
|
||||||
|
if (!(expression.type in expressionParsers)) {
|
||||||
|
throw Error('Invalid expression syntax');
|
||||||
|
}
|
||||||
return expressionParsers[expression.type](expression, model, isBackConvert, changedModel);
|
return expressionParsers[expression.type](expression, model, isBackConvert, changedModel);
|
||||||
}
|
}
|
||||||
|
@ -345,8 +345,7 @@ export class Binding {
|
|||||||
if (__UI_USE_EXTERNAL_RENDERER__) {
|
if (__UI_USE_EXTERNAL_RENDERER__) {
|
||||||
} else if (this.options.expression) {
|
} else if (this.options.expression) {
|
||||||
const changedModel = {};
|
const changedModel = {};
|
||||||
changedModel[bc.bindingValueKey] = value;
|
const targetInstance = this.target.get();
|
||||||
changedModel[bc.newPropertyValueKey] = value;
|
|
||||||
let sourcePropertyName = '';
|
let sourcePropertyName = '';
|
||||||
if (this.sourceOptions) {
|
if (this.sourceOptions) {
|
||||||
sourcePropertyName = this.sourceOptions.property;
|
sourcePropertyName = this.sourceOptions.property;
|
||||||
@ -354,13 +353,19 @@ export class Binding {
|
|||||||
sourcePropertyName = this.options.sourceProperty;
|
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 !== '') {
|
if (sourcePropertyName !== '') {
|
||||||
changedModel[sourcePropertyName] = value;
|
changedModel[sourcePropertyName] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateExpression = this.prepareExpressionForUpdate();
|
|
||||||
this.prepareContextForExpression(changedModel, updateExpression);
|
|
||||||
|
|
||||||
const expressionValue = this._getExpressionValue(updateExpression, true, changedModel);
|
const expressionValue = this._getExpressionValue(updateExpression, true, changedModel);
|
||||||
if (expressionValue instanceof Error) {
|
if (expressionValue instanceof Error) {
|
||||||
Trace.write((<Error>expressionValue).message, Trace.categories.Binding, Trace.messageType.error);
|
Trace.write((<Error>expressionValue).message, Trace.categories.Binding, Trace.messageType.error);
|
||||||
@ -373,17 +378,18 @@ export class Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _getExpressionValue(expression: string, isBackConvert: boolean, changedModel: any): any {
|
private _getExpressionValue(expression: string, isBackConvert: boolean, changedModel: any): any {
|
||||||
let result: any = '';
|
let result: any = null;
|
||||||
|
|
||||||
if (!__UI_USE_EXTERNAL_RENDERER__) {
|
if (!__UI_USE_EXTERNAL_RENDERER__) {
|
||||||
let context;
|
let context;
|
||||||
|
const targetInstance = this.target.get();
|
||||||
const addedProps = [];
|
const addedProps = [];
|
||||||
try {
|
try {
|
||||||
let exp;
|
let exp;
|
||||||
try {
|
try {
|
||||||
exp = parseExpression(expression);
|
exp = parseExpression(expression);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result = e;
|
result = new Error(e + ' at ' + targetInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exp) {
|
if (exp) {
|
||||||
@ -397,17 +403,15 @@ export class Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For expressions, there are also cases when binding must be updated after component is loaded (e.g. ListView)
|
// For expressions, there are also cases when binding must be updated after component is loaded (e.g. ListView)
|
||||||
if (this.prepareContextForExpression(context, expression, addedProps)) {
|
if (this.prepareContextForExpression(targetInstance, context, expression, addedProps)) {
|
||||||
result = convertExpressionToValue(exp, context, isBackConvert, changedModel ? changedModel : context);
|
result = convertExpressionToValue(exp, context, isBackConvert, changedModel ? changedModel : context);
|
||||||
} else {
|
} else {
|
||||||
const targetInstance = this.target.get();
|
|
||||||
targetInstance.off('loaded', this.loadedHandlerVisualTreeBinding, this);
|
targetInstance.off('loaded', this.loadedHandlerVisualTreeBinding, this);
|
||||||
targetInstance.on('loaded', this.loadedHandlerVisualTreeBinding, this);
|
targetInstance.on('loaded', this.loadedHandlerVisualTreeBinding, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage = 'Run-time error occured in file: ' + e.sourceURL + ' at line: ' + e.line + ' and column: ' + e.column;
|
result = new Error(e + ' at ' + targetInstance);
|
||||||
result = new Error(errorMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear added props
|
// Clear added props
|
||||||
@ -480,9 +484,7 @@ export class Binding {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private prepareContextForExpression(model: Object, expression: string, addedProps = []) {
|
private prepareContextForExpression(target: any, model: Object, expression: string, addedProps = []) {
|
||||||
const targetInstance = this.target.get();
|
|
||||||
|
|
||||||
let success = true;
|
let success = true;
|
||||||
let parentViewAndIndex: { view: ViewBase; index: number };
|
let parentViewAndIndex: { view: ViewBase; index: number };
|
||||||
let parentView;
|
let parentView;
|
||||||
@ -497,7 +499,7 @@ export class Binding {
|
|||||||
for (let i = 0; i < parentsArray.length; i++) {
|
for (let i = 0; i < parentsArray.length; i++) {
|
||||||
// This prevents later checks to mistake $parents[] for $parent
|
// This prevents later checks to mistake $parents[] for $parent
|
||||||
expressionCP = expressionCP.replace(parentsArray[i], '');
|
expressionCP = expressionCP.replace(parentsArray[i], '');
|
||||||
parentViewAndIndex = this.getParentView(targetInstance, parentsArray[i]);
|
parentViewAndIndex = this.getParentView(target, parentsArray[i]);
|
||||||
if (parentViewAndIndex.view) {
|
if (parentViewAndIndex.view) {
|
||||||
model[bc.parentsValueKey] = model[bc.parentsValueKey] || {};
|
model[bc.parentsValueKey] = model[bc.parentsValueKey] || {};
|
||||||
model[bc.parentsValueKey][parentViewAndIndex.index] = parentViewAndIndex.view.bindingContext;
|
model[bc.parentsValueKey][parentViewAndIndex.index] = parentViewAndIndex.view.bindingContext;
|
||||||
@ -509,7 +511,7 @@ export class Binding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (expressionCP.indexOf(bc.parentValueKey) > -1) {
|
if (expressionCP.indexOf(bc.parentValueKey) > -1) {
|
||||||
parentView = this.getParentView(targetInstance, bc.parentValueKey).view;
|
parentView = this.getParentView(target, bc.parentValueKey).view;
|
||||||
if (parentView) {
|
if (parentView) {
|
||||||
model[bc.parentValueKey] = parentView.bindingContext;
|
model[bc.parentValueKey] = parentView.bindingContext;
|
||||||
addedProps.push(bc.parentValueKey);
|
addedProps.push(bc.parentValueKey);
|
||||||
|
Reference in New Issue
Block a user