• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-angular / 23548401524

25 Mar 2026 03:12PM UTC coverage: 89.34% (-0.005%) from 89.345%
23548401524

Pull #17081

github

web-flow
Merge 6602fbfdf into d28544aee
Pull Request #17081: fix(query-builder): refactor expression tree change emission for consistency - master

14645 of 17230 branches covered (85.0%)

Branch coverage included in aggregate %.

14 of 16 new or added lines in 2 files covered. (87.5%)

1 existing line in 1 file now uncovered.

29580 of 32272 relevant lines covered (91.66%)

33892.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.13
/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts
1
import { booleanAttribute, ContentChild, EventEmitter, Output, TemplateRef, inject, ContentChildren, QueryList } from '@angular/core';
2
import {
3
    Component, Input, ViewChild, ElementRef, OnDestroy, HostBinding
4
} from '@angular/core';
5
import { Subject } from 'rxjs';
6
import {
7
    EntityType,
8
    FieldType,
9
    IExpressionTree,
10
    IQueryBuilderResourceStrings,
11
    QueryBuilderResourceStringsEN,
12
    recreateTree,
13
    IgxOverlayOutletDirective,
14
    getCurrentResourceStrings,
15
    onResourceChangeHandle
16
} from 'igniteui-angular/core';
17
import { IgxQueryBuilderTreeComponent } from './query-builder-tree.component';
18
import { IgxIconService } from 'igniteui-angular/icon';
19
import { editor } from '@igniteui/material-icons-extended';
20
import { IgxQueryBuilderSearchValueTemplateDirective } from './query-builder.directives';
21
import { IgxQueryBuilderSearchValueContext } from './query-builder.common';
22
import { IgxQueryBuilderHeaderComponent } from './query-builder-header.component';
23

24
/* wcElementTag: igc-query-builder */
25
/* blazorIndirectRender */
26
/**
27
 * A component used for operating with complex filters by creating or editing conditions
28
 * and grouping them using AND/OR logic.
29
 * It is used internally in the Advanced Filtering of the Grid.
30
 *
31
 * @example
32
 * ```html
33
 * <igx-query-builder [entities]="this.entities">
34
 * </igx-query-builder>
35
 * ```
36
 */
37
@Component({
38
    selector: 'igx-query-builder',
39
    templateUrl: './query-builder.component.html',
40
    imports: [IgxQueryBuilderTreeComponent]
41
})
42
export class IgxQueryBuilderComponent implements OnDestroy {
3✔
43
    protected iconService = inject(IgxIconService);
163✔
44

45
    /**
46
     * @hidden @internal
47
     */
48
    @HostBinding('class.igx-query-builder')
49
    public cssClass = 'igx-query-builder';
163✔
50

51
    /**
52
     * @hidden @internal
53
     */
54
    @HostBinding('style.display')
55
    public display = 'block';
163✔
56

57
    /**
58
     * Gets/sets whether the confirmation dialog should be shown when changing entity.
59
     * Default value is `true`.
60
     */
61
    @Input({ transform: booleanAttribute })
62
    public showEntityChangeDialog = true;
163✔
63

64
    /**
65
     * Gets the list of entities available for the IgxQueryBuilderComponent.
66
     *
67
     * Each entity describes a logical group of fields that can be used in queries.
68
     * An entity can optionally have child entities, allowing nested sub-queries.
69
     *
70
     * @returns An array of {@link EntityType} objects.
71
     */
72
    public get entities(): EntityType[] {
73
        return this._entities;
57,846✔
74
    }
75

76
    /**
77
     * Sets the list of entities for the IgxQueryBuilderComponent.
78
     * If the `expressionTree` is defined, it will be recreated with the new entities.
79
     *
80
     * Each entity should be an {@link EntityType} object describing the fields and optionally child entities.
81
     *
82
     * Example:
83
     * ```ts
84
     * [
85
     *   {
86
     *     name: 'Orders',
87
     *     fields: [{ field: 'OrderID', dataType: 'number' }],
88
     *     childEntities: [
89
     *       {
90
     *         name: 'OrderDetails',
91
     *         fields: [{ field: 'ProductID', dataType: 'number' }]
92
     *       }
93
     *     ]
94
     *   }
95
     * ]
96
     * ```
97
     *
98
     * @param entities - The array of entities to set.
99
     */
100
    @Input()
101
    public set entities(entities: EntityType[]) {
102
        if (entities !== this._entities) {
163✔
103
            if (entities && this.expressionTree) {
163!
104
                this._expressionTree = recreateTree(this._expressionTree, entities);
×
105
            }
106
        }
107
        this._entities = entities;
163✔
108
    }
109

110
    /**
111
     * Gets the list of fields for the QueryBuilder.
112
     *
113
     * @deprecated since version 19.1.0. Use the `entities` property instead.
114
     * @hidden
115
     */
116
    public get fields(): FieldType[] {
117
        return this._fields;
×
118
    }
119

120
    /**
121
     * Sets the list of fields for the QueryBuilder.
122
     * Automatically wraps them into a single entity to maintain backward compatibility.
123
     *
124
     * @param fields - The array of fields to set.
125
     * @deprecated since version 19.1.0. Use the `entities` property instead.
126
     * @hidden
127
     */
128
    @Input()
129
    public set fields(fields: FieldType[]) {
130
        if (fields) {
×
131
            this._fields = fields;
×
132
            this.entities = [
×
133
                {
134
                    name: null,
135
                    fields: fields
136
                }
137
            ];
138
        }
139
    }
140

141
    /**
142
    * Returns the expression tree.
143
    */
144
    public get expressionTree(): IExpressionTree {
145
        return this._expressionTree;
5,487✔
146
    }
147

148
    /**
149
     * Sets the expression tree.
150
     */
151
    @Input()
152
    public set expressionTree(expressionTree: IExpressionTree) {
153
        if (expressionTree !== this._expressionTree) {
142✔
154
            if (this.entities && expressionTree) {
92✔
155
                this._expressionTree = recreateTree(expressionTree, this.entities);
86✔
156
            } else {
157
                this._expressionTree = expressionTree;
6✔
158
            }
159
        }
160
    }
161

162
    /**
163
     * Gets the `locale` of the query builder.
164
     * If not set, defaults to application's locale.
165
     */
166
    @Input()
167
    public locale: string;
168

169
    /**
170
     * Sets the resource strings.
171
     * By default it uses EN resources.
172
     */
173
    @Input()
174
    public set resourceStrings(value: IQueryBuilderResourceStrings) {
175
        this._resourceStrings = Object.assign({}, this._resourceStrings, value);
63✔
176
    }
177

178
    /**
179
     * Returns the resource strings.
180
     */
181
    public get resourceStrings(): IQueryBuilderResourceStrings {
182
        return this._resourceStrings || this._defaultResourceStrings;
5,277✔
183
    }
184

185
    /**
186
     * Disables subsequent entity changes at the root level after the initial selection.
187
     */
188
    @Input()
189
    public disableEntityChange = false;
163✔
190

191
    /**
192
     * Sets/gets the search value template.
193
     */
194
    @Input()
195
    public set searchValueTemplate(template: TemplateRef<IgxQueryBuilderSearchValueContext>) {
196
        this._searchValueTemplate = template;
×
197
    }
198

199
    public get searchValueTemplate(): TemplateRef<IgxQueryBuilderSearchValueContext> {
200
        return this._searchValueTemplate || this.searchValueTemplateDirective?.template;
5,275✔
201
    }
202

203
    /**
204
     * Disables return fields changes at the root level.
205
     */
206
    @Input()
207
    public disableReturnFieldsChange = false;
163✔
208

209
    /**
210
     * Event fired as the expression tree is changed.
211
     *
212
     * ```html
213
     *  <igx-query-builder (expressionTreeChange)='onExpressionTreeChange()'></igx-query-builder>
214
     * ```
215
     */
216
    @Output()
217
    public expressionTreeChange = new EventEmitter<IExpressionTree>();
163✔
218

219
    /**
220
     * @hidden @internal
221
     */
222
    @ContentChild(IgxQueryBuilderSearchValueTemplateDirective)
223
    protected searchValueTemplateDirective: IgxQueryBuilderSearchValueTemplateDirective;
224

225

226

227
    /* contentChildren */
228
    /* blazorInclude */
229
    /* blazorTreatAsCollection */
230
    /* blazorCollectionName: QueryBuilderHeaderCollection */
231
    /* blazorCollectionItemName: QueryBuilderHeader */
232
    /* ngQueryListName: queryBuilderHeaderCollection */
233
    /** @hidden @internal */
234
    @ContentChildren(IgxQueryBuilderHeaderComponent)
235
    protected queryBuilderHeaderCollection: QueryList<IgxQueryBuilderHeaderComponent>;
236

237
    /**
238
     * @hidden @internal
239
     */
240
    @ViewChild(IgxQueryBuilderTreeComponent)
241
    public queryTree: IgxQueryBuilderTreeComponent;
242

243
    private destroy$ = new Subject<any>();
163✔
244
    private _resourceStrings: IQueryBuilderResourceStrings = null;
163✔
245
    private _defaultResourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN);
163✔
246
    private _expressionTree: IExpressionTree;
247
    private _fields: FieldType[];
248
    private _entities: EntityType[];
249
    private _shouldEmitTreeChange = true;
163✔
250
    private _searchValueTemplate: TemplateRef<IgxQueryBuilderSearchValueContext>;
251

252
    constructor() {
253
        this.registerSVGIcons();
163✔
254
        onResourceChangeHandle(this.destroy$, () => {
163✔
255
            this._defaultResourceStrings = getCurrentResourceStrings(QueryBuilderResourceStringsEN, false);
4✔
256
        }, this);
257
    }
258

259
    /**
260
     * Returns whether the expression tree can be committed in the current state.
261
     */
262
    public canCommit(): boolean {
263
        return this.queryTree?.canCommitCurrentState() === true;
28✔
264
    }
265

266
    /**
267
     * Commits the expression tree in the current state if it is valid. If not throws an exception.
268
     */
269
    public commit(): void {
270
        if (this.canCommit()) {
3✔
271
            this._shouldEmitTreeChange = false;
2✔
272
            this.queryTree.commitCurrentState();
2✔
273
            this._shouldEmitTreeChange = true;
2✔
274
        } else {
275
            throw new Error('Expression tree can\'t be committed in the current state. Use `canCommit` method to check if the current state is valid.');
1✔
276
        }
277
    }
278

279
    /**
280
     * Discards all unsaved changes to the expression tree.
281
     */
282
    public discard(): void {
283
        this.queryTree.cancelOperandEdit();
5✔
284
    }
285

286
    /**
287
     * @hidden @internal
288
     */
289
    public ngOnDestroy(): void {
290
        this.destroy$.next(true);
163✔
291
        this.destroy$.complete();
163✔
292
    }
293

294
    /**
295
     * @hidden @internal
296
     *
297
     * used by the grid
298
     */
299
    public setPickerOutlet(outlet?: IgxOverlayOutletDirective | ElementRef) {
300
        this.queryTree.setPickerOutlet(outlet);
59✔
301
    }
302

303
    /**
304
     * @hidden @internal
305
     *
306
     * used by the grid
307
     */
308
    public get isContextMenuVisible(): boolean {
309
        return this.queryTree.isContextMenuVisible;
×
310
    }
311

312
    /**
313
     * @hidden @internal
314
     *
315
     * used by the grid
316
     */
317
    public exitOperandEdit() {
318
        this.queryTree.exitOperandEdit();
20✔
319
    }
320

321
    /**
322
     * @hidden @internal
323
     *
324
     * used by the grid
325
     */
326
    public setAddButtonFocus() {
327
        this.queryTree.setAddButtonFocus();
53✔
328
    }
329

330
    private serializeExpressionTreeCallback(key: string, val: any) {
331
        if (key === 'externalObject') {
4,219!
NEW
332
            return undefined;
×
333
        }
334
        if (key === 'searchVal' && val instanceof Set) {
4,219✔
335
            // Ensure Set-based search values (e.g. for "in" conditions) are serialized correctly
336
            // JSON.stringify(new Set([...])) => '{}' by default, so convert to an array first
337
            return Array.from(val);
2✔
338
        }
339

340
        return val;
4,217✔
341
    }
342

343
    private getSerializableExpressionTree(tree: IExpressionTree): IExpressionTree {
344
        if (!tree) {
128!
NEW
UNCOV
345
            return tree;
×
346
        }
347

348
        return JSON.parse(JSON.stringify(tree, this.serializeExpressionTreeCallback));
128✔
349
    }
350

351
    protected onExpressionTreeChange(tree: IExpressionTree) {
352
        if (tree && this.entities && tree !== this._expressionTree) {
130✔
353
            this._expressionTree = recreateTree(tree, this.entities);
112✔
354
        } else {
355
            this._expressionTree = tree;
18✔
356
        }
357
        if (this._shouldEmitTreeChange) {
130✔
358
            this.expressionTreeChange.emit(this.getSerializableExpressionTree(this._expressionTree));
128✔
359
        }
360
    }
361

362
    private registerSVGIcons(): void {
363
        const editorIcons = editor as any[];
163✔
364

365
        editorIcons.forEach((icon) => {
163✔
366
            this.iconService.addSvgIconFromText(icon.name, icon.value, 'imx-icons');
9,780✔
367
            this.iconService.addIconRef(icon.name, 'default', {
9,780✔
368
                name: icon.name,
369
                family: 'imx-icons'
370
            });
371
        });
372

373
        const inIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M560-280H120v-400h720v120h-80v-40H200v240h360v80Zm-360-80v-240 240Zm560 200v-120H640v-80h120v-120h80v120h120v80H840v120h-80Z"/></svg>';
163✔
374
        this.iconService.addSvgIconFromText('in', inIcon, 'imx-icons');
163✔
375
        this.iconService.addIconRef('in', 'default', {
163✔
376
            name: 'in',
377
            family: 'imx-icons'
378
        });
379

380
        const notInIcon = '<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M560-280H120v-400h720v120h-80v-40H200v240h360v80Zm-360-80v-240 240Zm440 104 84-84-84-84 56-56 84 84 84-84 56 56-83 84 83 84-56 56-84-83-84 83-56-56Z"/></svg>';
163✔
381
        this.iconService.addSvgIconFromText('not-in', notInIcon, 'imx-icons');
163✔
382
        this.iconService.addIconRef('not-in', 'default', {
163✔
383
            name: 'not-in',
384
            family: 'imx-icons'
385
        });
386

387
        this.iconService.addIconRef('add', 'default', {
163✔
388
            name: 'add',
389
            family: 'material',
390
        });
391

392
        this.iconService.addIconRef('close', 'default', {
163✔
393
            name: 'close',
394
            family: 'material',
395
        });
396

397
        this.iconService.addIconRef('check', 'default', {
163✔
398
            name: 'check',
399
            family: 'material',
400
        });
401

402
        this.iconService.addIconRef('delete', 'default', {
163✔
403
            name: 'delete',
404
            family: 'material',
405
        });
406

407
        this.iconService.addIconRef('edit', 'default', {
163✔
408
            name: 'edit',
409
            family: 'material',
410
        });
411
    }
412
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc