Skip to content

Commit 49d0916

Browse files
authored
Allow to access survey elements properties in text processing and expressions fix #10692 (#10693)
* ValueGetterContextCore.getValue use inteface as one parameter * Add context into text processing * Support notification on dependencies property value changes
1 parent 3b6cd56 commit 49d0916

21 files changed

+377
-110
lines changed

packages/survey-core/src/base.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ export class Base implements IObjectValueContext {
213213
Base.currentDependencis.addDependency(target, property);
214214
}
215215
public dependencies: { [key: string]: ComputedUpdater } = {};
216+
private expressionDependencies: { [key: string]: { obj: Base, propertyName: string } } = {};
216217
public static get commentSuffix(): string {
217218
return settings.commentSuffix;
218219
}
@@ -338,7 +339,13 @@ export class Base implements IObjectValueContext {
338339
this.onPropertyValueChangedCallback = undefined;
339340
this.isDisposedValue = true;
340341
Object.keys(this.dependencies).forEach(key => this.dependencies[key].dispose());
341-
// this.dependencies = {};
342+
Object.keys(this.expressionDependencies).forEach(key => {
343+
const item = this.expressionDependencies[key];
344+
if (!item.obj.isDisposed) {
345+
item.obj.unRegisterFunctionOnPropertyValueChanged(item.propertyName, key);
346+
}
347+
});
348+
this.expressionDependencies = {};
342349
Object.keys(this.propertyHash).forEach(key => {
343350
const propVal = this.getPropertyValueCore(this.propertyHash, key);
344351
if (!!propVal && propVal.type == ComputedUpdater.ComputedUpdaterType) {
@@ -929,6 +936,20 @@ export class Base implements IObjectValueContext {
929936
public unRegisterFunctionOnPropertiesValueChanged(names: Array<string>, key: string = null): void {
930937
this.unregisterPropertyChangedHandlers(names, key);
931938
}
939+
public addPropertyDependency(obj: Base, propertyName: string): void {
940+
if (!obj || !propertyName) return;
941+
const id = obj.uniqueId + "_" + propertyName;
942+
if (!this.expressionDependencies[id]) {
943+
obj.registerFunctionOnPropertyValueChanged(propertyName, () => {
944+
this.onDependencyValueChanged(obj, propertyName);
945+
}, id);
946+
this.expressionDependencies[id] = { obj, propertyName };
947+
}
948+
}
949+
private onDependencyValueChanged(obj: Base, propertyName: string): void {
950+
this.runConditionCore(this.getDataFilteredProperties());
951+
this.locStrsChanged();
952+
}
932953
public createCustomLocalizableObj(name: string): LocalizableString {
933954
const locStr = this.getLocalizableString(name);
934955
if (locStr) return locStr;

packages/survey-core/src/conditionProcessValue.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Base } from "./base";
12
import { IQuestion } from "./base-interfaces";
23
import { Helpers, HashTable } from "./helpers";
34

@@ -6,6 +7,9 @@ export interface IValueGetterItem {
67
index?: number;
78
}
89
export interface IValueGetterInfo {
10+
obj?: IObjectValueContext;
11+
propObj?: any;
12+
propName?: string;
913
context?: IValueGetterContext;
1014
requireStrictCompare?: boolean;
1115
isFound?: boolean;
@@ -14,9 +18,17 @@ export interface IValueGetterInfo {
1418
export interface IObjectValueContext {
1519
getValueGetterContext(isUnwrapped?: boolean): IValueGetterContext;
1620
}
21+
export interface IValueGetterContextGetValueParams {
22+
path: Array<IValueGetterItem>;
23+
isRoot: boolean;
24+
index: number;
25+
createObjects?: boolean;
26+
isProperty?: boolean;
27+
}
1728
export interface IValueGetterContext {
18-
getValue(path: Array<IValueGetterItem>, isRoot: boolean, index: number, createObjects: boolean): IValueGetterInfo;
29+
getValue(params: IValueGetterContextGetValueParams): IValueGetterInfo;
1930
getTextValue?(name: string, value: any, isDisplayValue: boolean): string;
31+
getObj?(): Base;
2032
getRootObj?(): IObjectValueContext;
2133
getQuestion?(): IQuestion;
2234
}
@@ -31,6 +43,8 @@ export interface IReturnValue {
3143
isFound: boolean;
3244
value: any;
3345
question?: IQuestion;
46+
propObj?: any;
47+
propName?: string;
3448
strictCompare?: boolean;
3549
}
3650
export class ValueGetter {
@@ -60,6 +74,8 @@ export class ValueGetter {
6074
res.isFound = true;
6175
res.value = info.value;
6276
res.strictCompare = info.requireStrictCompare;
77+
res.propObj = info.propObj;
78+
res.propName = info.propName;
6379
if (info.context) {
6480
if (params.isText && info.context.getTextValue) {
6581
res.value = info.context.getTextValue(name, res.value, params.isDisplayValue);
@@ -119,8 +135,12 @@ export class ValueGetter {
119135
private run(name: string, context: IValueGetterContext, createObjects: boolean): any {
120136
if (!context) return undefined;
121137
let path = this.getPath(name);
122-
const info = context.getValue(path, true, -1, createObjects);
123-
return !!info && info.isFound ? info : undefined;
138+
const isProperty = path.length > 0 && path[0].name[0] === "$";
139+
if (isProperty) {
140+
path[0].name = path[0].name.substring(1);
141+
}
142+
const info = context.getValue({ path, isRoot: true, index: -1, createObjects, isProperty });
143+
return info?.isFound ? info : undefined;
124144
}
125145
public getPath(name: string): Array<IValueGetterItem> {
126146
const path: Array<IValueGetterItem> = [];
@@ -154,16 +174,21 @@ export class ValueGetter {
154174
}
155175
export class ValueGetterContextCore implements IValueGetterContext {
156176
constructor() {}
157-
public getValue(path: Array<IValueGetterItem>, isRoot: boolean, index: number, createObjects: boolean): IValueGetterInfo {
177+
public getValue(params: IValueGetterContextGetValueParams): IValueGetterInfo {
158178
let pIndex = 0;
179+
const path = params.path;
159180
const res: IValueGetterInfo = { isFound: false, value: this.getInitialvalue(), context: this };
160181
while(pIndex < path.length) {
161182
pIndex = this.checkValueByPath(path, pIndex, res);
162183
if (!res.isFound) return undefined;
184+
if (params.isProperty) {
185+
if (!!res.obj) return new PropertyGetterContext(res.obj).getValue({ path: path.slice(1), isRoot: false, index: -1, isProperty: true });
186+
if (!res.context) return undefined;
187+
}
163188
const item = path[pIndex];
164189
pIndex++;
165190
if (res.context !== this && !!res.context) {
166-
return res.context.getValue([].concat(path.slice(pIndex)), false, item.index, createObjects);
191+
return res.context.getValue({ path: path.slice(pIndex), isRoot: false, index: item.index, createObjects: params.createObjects, isProperty: params.isProperty });
167192
}
168193
if (item.index !== undefined) {
169194
this.updateItemByIndex(item.index, res);
@@ -259,21 +284,35 @@ export class VariableGetterContext extends ValueGetterContextCore {
259284
}
260285
}
261286
export class PropertyGetterContext extends VariableGetterContext {
287+
private lastObj: any;
288+
private lastName: string;
262289
constructor(private obj: any) {
263290
super(obj);
264291
}
292+
public getValue(params: IValueGetterContextGetValueParams): IValueGetterInfo {
293+
this.lastObj = undefined;
294+
this.lastName = undefined;
295+
const res = super.getValue(params);
296+
if (res?.isFound) {
297+
res.propObj = this.lastObj;
298+
res.propName = this.lastName;
299+
}
300+
return res;
301+
}
265302
protected getValueByItemCore(obj: any, name: string): any {
266303
if (!obj || !name) return undefined;
304+
this.lastObj = obj;
305+
this.lastName = name;
267306
return obj[name];
268307
}
269308
}
270309
export class VariableGetterContextEx extends VariableGetterContext {
271310
constructor(variables: HashTable<any>, private second: IValueGetterContext) {
272311
super(variables);
273312
}
274-
public getValue(path: Array<IValueGetterItem>, isRoot: boolean, index: number, createObjects: boolean): IValueGetterInfo {
275-
const res = super.getValue(path, isRoot, index, createObjects);
276-
return !this.second || res?.isFound ? res : this.second.getValue(path, isRoot, index, createObjects);
313+
public getValue(params: IValueGetterContextGetValueParams): IValueGetterInfo {
314+
const res = super.getValue(params);
315+
return !this.second || res?.isFound ? res : this.second.getValue(params);
277316
}
278317
}
279318

@@ -304,6 +343,12 @@ export class ProcessValue {
304343
public getValueInfo(valueInfo: any) {
305344
if (!!this.context) {
306345
const cRes = this.getValueInfoByContext(valueInfo.name);
346+
if (cRes.isFound) {
347+
const obj: Base = this.context.getObj ? this.context.getObj() : null;
348+
if (!!obj && !!cRes.propObj && (cRes.propObj instanceof Base)) {
349+
obj.addPropertyDependency(cRes.propObj, cRes.propName);
350+
}
351+
}
307352
valueInfo.value = cRes.value;
308353
valueInfo.hasValue = cRes.isFound;
309354
valueInfo.strictCompare = cRes.strictCompare;

packages/survey-core/src/itemvalue.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,26 @@ import { IShortcutText, ISurvey } from "./base-interfaces";
1313
import { settings } from "./settings";
1414
import { BaseAction } from "./actions/action";
1515
import { Question } from "./question";
16-
import { IObjectValueContext, IValueGetterContext, IValueGetterInfo, IValueGetterItem, PropertyGetterContext } from "./conditionProcessValue";
16+
import { IObjectValueContext, IValueGetterContext, IValueGetterContextGetValueParams, IValueGetterInfo, IValueGetterItem, PropertyGetterContext } from "./conditionProcessValue";
1717

1818
export class ItemValueGetterContext implements IValueGetterContext {
1919
constructor (protected item: ItemValue) {}
20-
getValue(path: Array<IValueGetterItem>, isRoot: boolean, index: number, createObjects: boolean): IValueGetterInfo {
21-
const name = path.length > 0 ? path[0].name : "";
20+
public getObj(): Base { return this.item; }
21+
public getValue(params: IValueGetterContextGetValueParams): IValueGetterInfo {
22+
const path = params.path;
23+
const name = path.length > 0 ? path[0].name.toLocaleLowerCase() : "";
2224
const expVar = settings.expressionVariables;
23-
if (path.length === 1) {
24-
if ([expVar.item, expVar.choice].indexOf(name.toLocaleLowerCase()) > -1) return { isFound: true, value: this.item.value, context: this };
25+
const isItemVar = [expVar.item, expVar.choice, expVar.self].indexOf(name) > -1;
26+
if (path.length === 1 && isItemVar) {
27+
return { isFound: true, value: this.item.value, context: this };
2528
}
26-
if (path.length > 1 && ([`$${expVar.item}`, `$${expVar.choice}`].indexOf(name.toLocaleLowerCase()) > -1)) {
27-
return new PropertyGetterContext(this.item).getValue(path.slice(1), true, -1, false);
29+
if (params.isProperty && path.length > 1 && isItemVar) {
30+
params.path = path.slice(1);
31+
return new PropertyGetterContext(this.item).getValue(params);
2832
}
2933
const owner: any = this.item.locOwner;
3034
if (owner && owner.getValueGetterContext) {
31-
return owner.getValueGetterContext().getValue(path, isRoot, index, createObjects);
35+
return owner.getValueGetterContext().getValue(params);
3236
}
3337
return undefined;
3438
}
@@ -61,7 +65,7 @@ export class ItemValue extends BaseAction implements ILocalizableOwner, IShortcu
6165
return !!this.locOwner ? this.locOwner.getRendererContext(locStr, this) : locStr;
6266
}
6367
public getProcessedText(text: string): string {
64-
return this.locOwner ? this.locOwner.getProcessedText(text) : text;
68+
return this.locOwner ? this.locOwner.getProcessedText(text, this) : text;
6569
}
6670

6771
public static get Separator() {

packages/survey-core/src/localizablestring.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ItemValue } from "./itemvalue";
99
export interface ILocalizableOwner {
1010
getLocale(): string;
1111
getMarkdownHtml(text: string, name: string, item?: any): string;
12-
getProcessedText(text: string): string;
12+
getProcessedText(text: string, context?: any): string;
1313
getRenderer(name: string, item?: ItemValue): string;
1414
getRendererContext(locStr: LocalizableString, item?: ItemValue): any;
1515
}

packages/survey-core/src/question.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { CssClassBuilder } from "./utils/cssClassBuilder";
1818
import { getElementWidth, isContainerVisible } from "./utils/utils";
1919
import { PopupModel } from "./popup";
2020
import { ConsoleWarnings } from "./console-warnings";
21-
import { IObjectValueContext, IValueGetterContext, IValueGetterInfo, IValueGetterItem, ProcessValue, PropertyGetterContext, ValueGetter, ValueGetterContextCore, VariableGetterContext } from "./conditionProcessValue";
21+
import { IObjectValueContext, IValueGetterContext, IValueGetterContextGetValueParams, IValueGetterInfo, IValueGetterItem, ProcessValue, PropertyGetterContext, ValueGetter, ValueGetterContextCore, VariableGetterContext } from "./conditionProcessValue";
2222
import { ITheme } from "./themes";
2323
import { DomDocumentHelper, DomWindowHelper } from "./global_variables_utils";
2424
import { ITextArea, TextAreaModel } from "./utils/text-area";
@@ -55,21 +55,29 @@ interface ITriggerExpressionInfo {
5555

5656
export class QuestionValueGetterContext implements IValueGetterContext {
5757
constructor (protected question: Question, protected isUnwrapped?: boolean) {}
58-
getValue(path: Array<IValueGetterItem>, isRoot: boolean, index: number, createObjects: boolean): IValueGetterInfo {
58+
public getObj(): Base { return this.question; }
59+
public getValue(params: IValueGetterContextGetValueParams): IValueGetterInfo {
60+
const path = params.path;
61+
const index = params.index;
5962
const expVar = settings.expressionVariables;
60-
if (path.length === 0 || (path.length === 1 && path[0].name === expVar.question)) return this.getQuestionValue(index);
61-
//TODO make it more generic by supporting $name.property
62-
if (path.length > 1 && path[0].name === "$" + expVar.question) {
63-
return new PropertyGetterContext(this.question).getValue(path.slice(1), isRoot, index, createObjects);
64-
}
65-
if (path.length > 1 && path[0].name === "$" + expVar.parent && !!this.question.parentQuestion) {
66-
return new PropertyGetterContext(this.question.parentQuestion).getValue(path.slice(1), isRoot, index, createObjects);
63+
if (params.isProperty && path.length > 1) {
64+
params.path = path.slice(1);
65+
params.isRoot = false;
66+
if (path[0].name === expVar.self) {
67+
return new PropertyGetterContext(this.question).getValue(params);
68+
}
69+
if (path[0].name === expVar.parent && !!this.question.parentQuestion) {
70+
return new PropertyGetterContext(this.question.parentQuestion).getValue(params);
71+
}
6772
}
73+
if (path.length === 0 || (path.length === 1 && path[0].name === expVar.self)) return this.getQuestionValue(index);
6874
if (path.length > 1 && path[0].name === expVar.panel) {
75+
params.isRoot = false;
6976
const panel: any = this.question.parent;
7077
if (panel && panel.isPanel) {
7178
path.shift();
72-
return new QuestionArrayGetterContext(panel.questions).getValue(path, false, index, createObjects);
79+
return params.isProperty ? new PropertyGetterContext(panel).getValue(params) :
80+
new QuestionArrayGetterContext(panel.questions).getValue(params);
7381
}
7482
}
7583
if (!this.question.isEmpty()) {
@@ -78,7 +86,8 @@ export class QuestionValueGetterContext implements IValueGetterContext {
7886
if (!Array.isArray(val || index >= val.length)) return undefined;
7987
val = val[index];
8088
}
81-
return new VariableGetterContext(val).getValue(path, false, index, createObjects);
89+
params.isProperty = false;
90+
return new VariableGetterContext(val).getValue(params);
8291
}
8392
return undefined;
8493
}
@@ -90,7 +99,7 @@ export class QuestionValueGetterContext implements IValueGetterContext {
9099
getQuestion(): IQuestion { return this.question; }
91100
protected getSurveyValue(path: Array<IValueGetterItem>, index?: number): IValueGetterInfo {
92101
const survey = this.question.getSurvey();
93-
if (survey) return (<any>survey).getValueGetterContext().getValue(path, false, index, false);
102+
if (survey) return (<any>survey).getValueGetterContext().getValue({ path, isRoot: false, index });
94103
return undefined;
95104
}
96105
private getQuestionValue(index: number): IValueGetterInfo {
@@ -117,7 +126,7 @@ export abstract class QuestionItemValueGetterContext extends ValueGetterContextC
117126
if (!!name && q.valuePropertyName === name && !!objValue && objValue.hasOwnProperty(name)) {
118127
return { isFound: true, value: objValue[name], context: q.getValueGetterContext() };
119128
}
120-
const res = q.getValueGetterContext().getValue(path, false, this.getIndex(), false);
129+
const res = q.getValueGetterContext().getValue({ path, isRoot: false, index: this.getIndex() });
121130
if (!!res && res.isFound) return res;
122131
}
123132
return undefined;
@@ -158,6 +167,7 @@ export class QuestionArrayGetterContext extends ValueGetterContextCore {
158167
const qName = q.getFilteredName().toLocaleLowerCase();
159168
if (qName.toLocaleLowerCase() === lowName) {
160169
res.isFound = true;
170+
res.obj = q;
161171
res.context = q.getValueGetterContext(qName.endsWith(unWrappedNameSuffix));
162172
break;
163173
}

packages/survey-core/src/question_custom.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { SurveyError } from "./survey-error";
2323
import { CustomError } from "./error";
2424
import { ConsoleWarnings } from "./console-warnings";
2525
import { settings } from "./settings";
26-
import { IValueGetterContext, IValueGetterInfo, IValueGetterItem } from "./conditionProcessValue";
26+
import { IValueGetterContext, IValueGetterContextGetValueParams, IValueGetterInfo, PropertyGetterContext } from "./conditionProcessValue";
2727
import { ValidationContext } from "./question";
2828

2929
/**
@@ -1069,14 +1069,18 @@ export class CompositeValueGetterContext extends QuestionValueGetterContext {
10691069
constructor (protected question: Question) {
10701070
super(question);
10711071
}
1072-
public getValue(path: Array<IValueGetterItem>, isRoot: boolean, index: number, createObjects: boolean): IValueGetterInfo {
1072+
public getObj(): Base { return this.question; }
1073+
public getValue(params: IValueGetterContextGetValueParams): IValueGetterInfo {
10731074
const cq = <QuestionCompositeModel>this.question;
1074-
if (path.length > 0 && (path.length > 1 || !isRoot)) {
1075+
const path = params.path;
1076+
if (path.length > 0 && (path.length > 1 || !params.isRoot)) {
10751077
const isCompPrefix = path[0].name === settings.expressionVariables.composite;
10761078
if (isCompPrefix) {
10771079
path.shift();
10781080
}
1079-
return new QuestionArrayGetterContext(cq.contentPanel.questions).getValue(path, false, index, createObjects);
1081+
params.isRoot = false;
1082+
return params.isProperty ? new PropertyGetterContext(cq.contentPanel).getValue(params) :
1083+
new QuestionArrayGetterContext(cq.contentPanel.questions).getValue(params);
10801084
}
10811085
return { isFound: false };
10821086
}

packages/survey-core/src/question_imagepicker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ export class ImageItemValue extends ChoiceItem implements ILocalizableOwner {
4747
getRendererContext(locStr: LocalizableString): any {
4848
return !!this.locOwner ? this.locOwner.getRendererContext(locStr) : locStr;
4949
}
50-
getProcessedText(text: string): string {
51-
return !!this.locOwner ? this.locOwner.getProcessedText(text) : text;
50+
getProcessedText(text: string, context?: any): string {
51+
return !!this.locOwner ? this.locOwner.getProcessedText(text, context) : text;
5252
}
5353

5454
public onErrorHandler(): void {

0 commit comments

Comments
 (0)