Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {

this._editedExpression = null;
if (!this.parentExpression) {
this.expressionTreeChange.emit(this._expressionTree);
this.emitExpressionTreeChange();
}

this.rootGroup = null;
Expand Down Expand Up @@ -699,7 +699,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {

if (this._expressionTree && !this.parentExpression) {
this._expressionTree.returnFields = value.length === this.fields.length ? ['*'] : value;
this.expressionTreeChange.emit(this._expressionTree);
this.emitExpressionTreeChange();
}
}
}
Expand Down Expand Up @@ -892,7 +892,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {

this._expressionTree = this.createExpressionTreeFromGroupItem(this.rootGroup, this.selectedEntity?.name, this.selectedReturnFields);
if (!this.parentExpression) {
this.expressionTreeChange.emit(this._expressionTree);
this.emitExpressionTreeChange();
}
}

Expand Down Expand Up @@ -933,7 +933,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
}

if (!this.parentExpression && !skipEmit) {
this.expressionTreeChange.emit(this._expressionTree);
this.emitExpressionTreeChange();
}
}

Expand Down Expand Up @@ -1523,7 +1523,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
}

if (!this.parentExpression) {
this.expressionTreeChange.emit(this._expressionTree);
this.emitExpressionTreeChange();
}
}

Expand Down Expand Up @@ -1714,12 +1714,16 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy {
}, this._focusDelay);
}

private emitExpressionTreeChange(): void {
this.expressionTreeChange.emit(this._expressionTree);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this going into a separate method?

}

private init() {
this.cancelOperandAdd();
this.cancelOperandEdit();

// Ignore values of certain properties for the comparison
const propsToIgnore = ['parent', 'hovered', 'ignoreCase', 'inEditMode', 'inAddMode'];
const propsToIgnore = ['parent', 'hovered', 'ignoreCase', 'inEditMode', 'inAddMode', 'externalObject'];
const propsReplacer = function replacer(key, value) {
if (propsToIgnore.indexOf(key) >= 0) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,45 @@ describe('IgxQueryBuilder', () => {
}));
});

describe('Serialization', () => {
it('Should serialize Set searchVal as array when emitting expressionTreeChange.', () => {
const tree = new FilteringExpressionsTree(FilteringLogic.And, null, 'Orders', ['*']);
tree.filteringOperands.push({
fieldName: 'OrderId',
condition: IgxNumberFilteringOperand.instance().condition('in'),
conditionName: 'in',
searchVal: new Set([1])
} as any);

spyOn(queryBuilder.expressionTreeChange, 'emit');
(queryBuilder as any).onExpressionTreeChange(tree);

const emittedTree = (queryBuilder.expressionTreeChange.emit as jasmine.Spy).calls.mostRecent().args[0] as IExpressionTree;
const emittedExpression = emittedTree.filteringOperands[0] as any;
expect(Array.isArray(emittedExpression.searchVal)).toBeTrue();
expect(emittedExpression.searchVal).toEqual([1]);
});

it('Should emit a deep-cloned serializable tree when expressionTreeChange fires.', () => {
const tree = new FilteringExpressionsTree(FilteringLogic.And, null, 'Orders', ['*']);
tree.filteringOperands.push({
fieldName: 'OrderId',
condition: IgxNumberFilteringOperand.instance().condition('greaterThan'),
conditionName: 'greaterThan',
searchVal: 5
} as any);

spyOn(queryBuilder.expressionTreeChange, 'emit');
(queryBuilder as any).onExpressionTreeChange(tree);

const emittedTree = (queryBuilder.expressionTreeChange.emit as jasmine.Spy).calls.mostRecent().args[0] as IExpressionTree;
expect(emittedTree).not.toBe(queryBuilder.expressionTree);

(emittedTree.filteringOperands[0] as any).conditionName = 'equals';
expect((queryBuilder.expressionTree.filteringOperands[0] as any).conditionName).toBe('greaterThan');
});
});

describe('Interactions', () => {
it('Should correctly initialize a newly added \'And\' group.', fakeAsync(() => {
QueryBuilderFunctions.selectEntityInEditModeExpression(fix, 1); // Select 'Orders' entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,14 +327,35 @@ export class IgxQueryBuilderComponent implements OnDestroy {
this.queryTree.setAddButtonFocus();
}

private serializeExpressionTreeCallback(key: string, val: unknown): unknown {
if (key === 'externalObject') {
return undefined;
}
if (key === 'searchVal' && val instanceof Set) {
// Ensure Set-based search values (e.g. for "in" conditions) are serialized correctly
// JSON.stringify(new Set([...])) => '{}' by default, so convert to an array first
return Array.from(val);
}

return val;
}

private getSerializableExpressionTree(tree: IExpressionTree): IExpressionTree {
if (!tree) {
return tree;
}

return JSON.parse(JSON.stringify(tree, this.serializeExpressionTreeCallback));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is introducing a behavior change. Let's say that any of the filtering operands is having custom logic as condition. The condition contains logic function that would be lost in the JSON.stringify. So if we bind to the expressionTreeChange of the query builder and change the grids expressionTree in the handler an error is thrown:

Image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not tested this but what would happen if there are dates as searchValue. They might work correct because we do a lot of date parsing but .stringify would parse them to ISO string. Could you confirm that this works as well?

}

protected onExpressionTreeChange(tree: IExpressionTree) {
if (tree && this.entities && tree !== this._expressionTree) {
this._expressionTree = recreateTree(tree, this.entities);
} else {
this._expressionTree = tree;
}
if (this._shouldEmitTreeChange) {
this.expressionTreeChange.emit(tree);
this.expressionTreeChange.emit(this.getSerializableExpressionTree(this._expressionTree));
}
}

Expand Down Expand Up @@ -389,4 +410,3 @@ export class IgxQueryBuilderComponent implements OnDestroy {
});
}
}

10 changes: 9 additions & 1 deletion src/app/query-builder/query-builder.sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,15 @@ export class QueryBuilderComponent implements OnInit {
// this.expressionTree = tree;
// this.onChange();
}
return tree ? JSON.stringify(tree, null, 2) : 'Please add an expression!';
return tree ? JSON.stringify(tree, this.serializeExpressionTreeCallback, 2) : 'Please add an expression!';
}

// JSON.stringify serializes Set as {}, so convert Set-based searchVal to array to preserve values in the printed output.
private serializeExpressionTreeCallback(key: string, val: any) {
if (key === 'searchVal' && val instanceof Set) {
return Array.from(val);
}
return val;
}

public canCommitExpressionTree() {
Expand Down
Loading