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

IgniteUI / igniteui-angular / 26023601418

18 May 2026 08:57AM UTC coverage: 4.854% (-85.3%) from 90.174%
26023601418

Pull #17281

github

web-flow
Merge e7ce7a18e into 5a85df190
Pull Request #17281: feat: Added virtual scroll component and sample implementation

400 of 17347 branches covered (2.31%)

Branch coverage included in aggregate %.

63 of 222 new or added lines in 4 files covered. (28.38%)

27932 existing lines in 341 files now uncovered.

2022 of 32547 relevant lines covered (6.21%)

0.72 hits per line

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

2.94
/projects/igniteui-angular/core/src/data-operations/grid-sorting-strategy.ts
1
import { cloneArray, columnFieldPath, parseDate, resolveNestedPath } from '../core/utils';
2
import { IGroupByExpandState } from './groupby-expand-state.interface';
3
import { IGroupByRecord } from './groupby-record.interface';
4
import { IGroupingState } from './groupby-state.interface';
5
import { IGroupingExpression } from './grouping-expression.interface';
6
import { IGroupByResult } from './grouping-result.interface';
7
import { getHierarchy, isHierarchyMatch } from './operations';
8
import { DefaultSortingStrategy, ISortingExpression, SortingDirection } from './sorting-strategy';
9
import type { GridTypeBase } from './grid-types';
10

11
const DATE_TYPE = 'date';
3✔
12
const TIME_TYPE = 'time';
3✔
13
const DATE_TIME_TYPE = 'dateTime';
3✔
14
const STRING_TYPE = 'string';
3✔
15

16
/**
17
 * Represents a sorting strategy for the grid data
18
 * Contains a single method sort that sorts the provided data based on the given sorting expressions
19
 */
20
export interface IGridSortingStrategy {
21
    /* blazorCSSuppress */
22
    /**
23
    * `data`: The array of data to be sorted. Could be of any type.
24
    * `expressions`: An array of sorting expressions that define the sorting rules. The expression contains information like file name, whether the letter case should be taken into account, etc.
25
    * `grid`: (Optional) The instance of the grid where the sorting is applied.
26
    * Returns a new array with the data sorted according to the sorting expressions.
27
    */
28
    sort(data: any[], expressions: ISortingExpression[], grid?: GridTypeBase): any[];
29
}
30

31
/**
32
 * Represents a grouping strategy for the grid data, extending the Sorting Strategy interface (contains a sorting method).
33
 */
34
export interface IGridGroupingStrategy extends IGridSortingStrategy {
35
    /* blazorCSSuppress */
36
    /**
37
     * The method groups the provided data based on the given grouping state and returns the result.
38
     * `data`: The array of data to be grouped. Could be of any type.
39
     * `state`: The grouping state that defines the grouping settings and expressions.
40
     * `grid`: (Optional) The instance of the grid where the grouping is applied.
41
     * `groupsRecords`: (Optional) An array that holds the records for each group.
42
     * `fullResult`: (Optional) The complete result of grouping including groups and summary data.
43
     * Returns an object containing the result of the grouping operation.
44
     */
45
    groupBy(data: any[], state: IGroupingState, grid?: any, groupsRecords?: any[], fullResult?: IGroupByResult): IGroupByResult;
46
}
47

48
/**
49
 * Represents internal sorting expression that extends the public one.
50
 * Contains boolean properties that represent the type of the column that is being sorted.
51
 * @internal
52
 */
53
interface IGridInternalSortingExpression extends ISortingExpression {
54
    isDate: boolean;
55
    isTime: boolean;
56
    isString: boolean;
57
}
58

59
/**
60
 * Stack item represents a frame.
61
 * Each frame needs:
62
 * - data: The subset of records to process at this level.
63
 * - level: The current grouping level.
64
 * - parentGroup: The parent IGroupByRecord for groups created in this frame.
65
 * - currentIndex: The index within 'data' to start processing.
66
 * - isExpandingChildren: Flag to indicate if children generated by this group should be added to `result` and `metadata`.
67
 * @internal
68
 */
69
interface StackFrame {
70
    data: any[];
71
    level: number;
72
    parentGroup: IGroupByRecord | null;
73
    currentIndex: number;
74
    isExpandingChildren: boolean;
75
}
76

77
/**
78
 * Represents a class implementing the IGridSortingStrategy interface.
79
 * It provides sorting functionality for grid data based on sorting expressions.
80
 */
81
export class IgxSorting implements IGridSortingStrategy {
82
    /* blazorSuppress */
83
    /**
84
   * Sorts the provided data based on the given sorting expressions.
85
   * `data`: The array of data to be sorted.
86
   * `expressions`: An array of sorting expressions that define the sorting rules. The expression contains information like file name, whether the letter case should be taken into account, etc.
87
   * `grid`: (Optional) The instance of the grid where the sorting is applied.
88
   * Returns a new array with the data sorted according to the sorting expressions.
89
   */
90
    public sort(data: any[], expressions: ISortingExpression[], grid?: GridTypeBase): any[] {
UNCOV
91
        return this.sortData(data, expressions, grid);
×
92
    }
93

94
    /**
95
     * Retrieves the value of the specified field from the given object, considering date and time data types.
96
     * `key`: The key of the field to retrieve.
97
     * `isDate`: (Optional) Indicates if the field is of type Date.
98
     * `isTime`: (Optional) Indicates if the field is of type Time.
99
     * Returns the value of the specified field in the data object.
100
     * @internal
101
     */
102
    protected getFieldValue<T>(obj: T, key: string, isDate = false, isTime = false) {
×
UNCOV
103
        let resolvedValue = resolveNestedPath(obj, columnFieldPath(key));
×
UNCOV
104
        if (isDate || isTime) {
×
UNCOV
105
            const date = parseDate(resolvedValue);
×
UNCOV
106
            if (date && isDate && isTime) {
×
UNCOV
107
                resolvedValue = date;
×
UNCOV
108
            } else if (date && isDate && !isTime) {
×
UNCOV
109
                resolvedValue = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0);
×
UNCOV
110
            } else if (date && isTime && !isDate) {
×
UNCOV
111
                resolvedValue = new Date(new Date().setHours(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()));
×
112
            }
113
        }
UNCOV
114
        return resolvedValue;
×
115
    }
116

117
    /**
118
   * Sorts the provided data array based on the given sorting expressions.
119
   * The method can be used when multiple sorting is performed, going through each one
120
   * Returns a new array with the data sorted according to the sorting expressions.
121
   * @internal
122
   */
123
    private sortData<T>(
124
        data: T[],
125
        expressions: ISortingExpression[],
126
        grid: GridTypeBase
127
    ): T[] {
UNCOV
128
        const sortingExpressions = this.prepareExpressions(expressions, grid);
×
129

UNCOV
130
        if (data.length <= 1) {
×
UNCOV
131
            return data;
×
132
        }
133

UNCOV
134
        for (let i = sortingExpressions.length - 1; i >= 0; i--) {
×
UNCOV
135
            data = sortingExpressions[i].strategy.sort(data, sortingExpressions[i].fieldName, sortingExpressions[i].dir, sortingExpressions[i].ignoreCase, this.getFieldValue, sortingExpressions[i].isDate, sortingExpressions[i].isTime, grid)
×
136
        }
137

UNCOV
138
        return data;
×
139
    }
140

141
    private prepareExpressions(expressions: ISortingExpression[], grid: GridTypeBase): IGridInternalSortingExpression[] {
UNCOV
142
        const multipleSortingExpressions: IGridInternalSortingExpression[] = [];
×
UNCOV
143
        for (const expr of expressions) {
×
UNCOV
144
            if (expr.dir === SortingDirection.None) {
×
UNCOV
145
                continue;
×
146
            }
UNCOV
147
            if (!expr.strategy) {
×
UNCOV
148
                expr.strategy = DefaultSortingStrategy.instance();
×
149
            }
UNCOV
150
            const column = grid?.getColumnByName(expr.fieldName);
×
UNCOV
151
            const isDate = column?.dataType === DATE_TYPE || column?.dataType === DATE_TIME_TYPE;
×
UNCOV
152
            const isTime = column?.dataType === TIME_TYPE || column?.dataType === DATE_TIME_TYPE;
×
UNCOV
153
            const isString = column?.dataType === STRING_TYPE;
×
UNCOV
154
            multipleSortingExpressions.push({ ...expr, isDate, isTime, isString })
×
155
        }
UNCOV
156
        return multipleSortingExpressions;
×
157
    }
158
}
159

160
/**
161
 * Represents a class implementing the IGridGroupingStrategy interface and extending the IgxSorting class.
162
 * It provides a method to group data based on the given grouping state.
163
 */
164
export class IgxGrouping extends IgxSorting implements IGridGroupingStrategy {
165
    /* blazorSuppress */
166
    /**
167
     * Groups the provided data based on the given grouping state.
168
     * Returns an object containing the result of the grouping operation.
169
     */
170
    public groupBy(data: any[], state: IGroupingState, grid?: any,
171
        groupsRecords?: any[], fullResult: IGroupByResult = { data: [], metadata: [] }): IGroupByResult {
×
UNCOV
172
        const grouping = this.groupData(data, state, grid, groupsRecords, fullResult);
×
UNCOV
173
        grid?.groupingPerformedSubject.next();
×
UNCOV
174
        return {
×
175
            data: grouping.data,
176
            metadata: grouping.metadata
177
        };
178
    }
179

180
    /**
181
     * Groups the provided data based on the given grouping state.
182
     * Changes groupsRecords and fullResult collections by reference.
183
     * Returns an array containing the visible grouped result.
184
     * @internal
185
     */
186
    protected groupData(
187
        data: any[],
188
        state: IGroupingState,
189
        grid: GridTypeBase = null,
×
190
        groupsRecords: any[] = [],
×
191
        fullResult: IGroupByResult
192
    ): IGroupByResult {
193

UNCOV
194
        const expressions = state.expressions;
×
UNCOV
195
        const expansion = state.expansion;
×
196

197
        // This holds the final visible data (the rows that are expanded).
UNCOV
198
        const result: any[] = [];
×
199

200
        // This holds the group rows for each record in the result array. Used in grid for information when scrolling.
UNCOV
201
        const metadata: IGroupByRecord[] = [];
×
202

203
        // Initialize the stack with the root level processing.
UNCOV
204
        const initialFrame: StackFrame = {
×
205
            data: data,
206
            level: 0,
207
            parentGroup: null,
208
            currentIndex: 0,
209
            isExpandingChildren: true
210
        };
UNCOV
211
        const stack: StackFrame[] = [initialFrame];
×
212

UNCOV
213
        while (stack.length > 0) {
×
UNCOV
214
            const currentFrame = stack[stack.length - 1]; // Peek at the top of the stack
×
215

UNCOV
216
            const { data: currentData, level, parentGroup, currentIndex, isExpandingChildren } = currentFrame;
×
217

218
            // If we've processed all data in this frame, pop it.
UNCOV
219
            if (currentIndex >= currentData.length) {
×
UNCOV
220
                stack.pop();
×
UNCOV
221
                continue;
×
222
            }
223

224
            // Process the next group at the current level
UNCOV
225
            const column = grid ? grid.getColumnByName(expressions[level].fieldName) : null;
×
UNCOV
226
            const isDate = column?.dataType === DATE_TYPE || column?.dataType === DATE_TIME_TYPE;
×
UNCOV
227
            const isTime = column?.dataType === TIME_TYPE || column?.dataType === DATE_TIME_TYPE;
×
UNCOV
228
            const isString = column?.dataType === STRING_TYPE;
×
229

230
            // Next block of grouped records for the expression of the current level
UNCOV
231
            const group = this.groupedRecordsByExpression(
×
232
                currentData,
233
                currentIndex,
234
                expressions[level],
235
                isDate,
236
                isTime,
237
                isString,
238
                column?.groupingComparer
239
            );
240

241
            // Create the group row
UNCOV
242
            const groupRow: IGroupByRecord = {
×
243
                expression: expressions[level],
244
                level,
245
                records: cloneArray(group),
246
                value: this.getFieldValue(group[0], expressions[level].fieldName, isDate, isTime),
247
                groupParent: parentGroup,
248
                groups: [],
249
                height: grid ? grid.renderedRowHeight : null,
×
250
                column
251
            };
252

253
            // Link to parent's groups list
UNCOV
254
            if (parentGroup) {
×
UNCOV
255
                parentGroup.groups.push(groupRow);
×
256
            } else {
UNCOV
257
                groupsRecords.push(groupRow)
×
258
            }
259

260
            // Determine expansion state for this groupRow
UNCOV
261
            const hierarchy = getHierarchy(groupRow);
×
UNCOV
262
            const expandState: IGroupByExpandState = expansion.find((s) =>
×
UNCOV
263
                isHierarchyMatch(
×
264
                    s.hierarchy || [{ fieldName: groupRow.expression.fieldName, value: groupRow.value }],
×
265
                    hierarchy,
266
                    expressions
267
                )
268
            );
UNCOV
269
            const expandedForThisGroup = expandState ? expandState.expanded : state.defaultExpanded;
×
270

271
            // Add the group row to the full result set
UNCOV
272
            fullResult.data.push(groupRow);
×
UNCOV
273
            fullResult.metadata.push(null);
×
274

275
            // Add the group row to the visible results (if its parent was expanded or it's a root group)
UNCOV
276
            if (isExpandingChildren) {
×
UNCOV
277
                result.push(groupRow);
×
UNCOV
278
                metadata.push(null);
×
279
            }
280

281
            // Advance the current frame's index for the next iteration of its loop
UNCOV
282
            currentFrame.currentIndex += group.length;
×
283

UNCOV
284
            if (level < expressions.length - 1) {
×
285
                // If there are more levels to group, push a new frame onto the stack
UNCOV
286
                const nextFrame: StackFrame = {
×
287
                    data: group, // The records of the current group become the data for the next level
288
                    level: level + 1,
289
                    parentGroup: groupRow, // The current group row is the parent for the next level
290
                    currentIndex: 0,
291
                    isExpandingChildren: isExpandingChildren && expandedForThisGroup // Children are expanded only if this group is expanded AND parent is expanded
×
292
                };
UNCOV
293
                stack.push(nextFrame);
×
294
            } else {
295
                // This is the leaf level, add individual items to fullResult and conditionally to result/metadata
UNCOV
296
                for (const groupItem of group) {
×
UNCOV
297
                    fullResult.metadata.push(groupRow); // The metadata for an item is its immediate parent group row.
×
UNCOV
298
                    fullResult.data.push(groupItem);
×
UNCOV
299
                    if (isExpandingChildren && expandedForThisGroup) {
×
300
                        // Add to result and metadata only if expanded
UNCOV
301
                        metadata.push(groupRow);
×
UNCOV
302
                        result.push(groupItem);
×
303
                    }
304
                }
305
            }
306
        }
307

UNCOV
308
        return { data: result, metadata };
×
309
    }
310

311
    /**
312
     * Groups the records in the provided data array based on the given grouping expression.
313
     * `groupingComparer`: (Optional) A custom grouping comparator to determine the members of the group.
314
     * Returns an array containing the records that belong to the group.
315
     * @internal
316
     */
317
    private groupedRecordsByExpression<T>(
318
        data: T[],
319
        index: number,
320
        expression: IGroupingExpression,
321
        isDate = false,
×
322
        isTime = false,
×
323
        isString: boolean,
324
        groupingComparer?: (a: any, b: any, currRec: any, groupRec: any) => number
325
    ): T[] {
UNCOV
326
        const res: T[] = [];
×
UNCOV
327
        const key = expression.fieldName;
×
UNCOV
328
        const len = data.length;
×
UNCOV
329
        const groupRecord = data[index];
×
UNCOV
330
        let groupValue = this.getFieldValue(groupRecord, key, isDate, isTime);
×
UNCOV
331
        if (expression.ignoreCase && isString && groupValue) {
×
332
            // when column's dataType is string but the value is number
UNCOV
333
            groupValue = groupValue.toString().toLowerCase();
×
334
        }
UNCOV
335
        res.push(groupRecord);
×
UNCOV
336
        const comparer = expression.groupingComparer || groupingComparer || DefaultSortingStrategy.instance().compareValues;
×
UNCOV
337
        for (let i = index + 1; i < len; i++) {
×
UNCOV
338
            const currRec = data[i];
×
UNCOV
339
            let fieldValue = this.getFieldValue(currRec, key, isDate, isTime);
×
UNCOV
340
            if (expression.ignoreCase && isString && fieldValue) {
×
341
                // when column's dataType is string but the value is number
UNCOV
342
                fieldValue = fieldValue.toString().toLowerCase();
×
343
            }
UNCOV
344
            if (comparer(fieldValue, groupValue, currRec, groupRecord) === 0) {
×
UNCOV
345
                res.push(currRec);
×
346
            } else {
UNCOV
347
                break;
×
348
            }
349
        }
UNCOV
350
        return res;
×
351
    }
352
}
353

354
/* csSuppress */
355
/**
356
 * Represents a class implementing the IGridSortingStrategy interface with a no-operation sorting strategy.
357
 * It performs no sorting and returns the data as it is.
358
 */
359
export class NoopSortingStrategy implements IGridSortingStrategy {
360
    private static _instance: NoopSortingStrategy = null;
3✔
361

362
    private constructor() { }
363

364
    public static instance(): NoopSortingStrategy {
UNCOV
365
        return this._instance || (this._instance = new NoopSortingStrategy());
×
366
    }
367

368
    /* csSuppress */
369
    public sort(data: any[]): any[] {
UNCOV
370
        return data;
×
371
    }
372
}
373

374
/**
375
 * Represents a class extending the IgxSorting class
376
 * Provides custom data record sorting.
377
 */
378
export class IgxDataRecordSorting extends IgxSorting {
379
    /**
380
    * Overrides the base method to retrieve the field value from the data object instead of the record object.
381
    * Returns the value of the specified field in the data object.
382
    */
383
    protected override getFieldValue(obj: any, key: string, isDate = false, isTime = false): any {
×
UNCOV
384
        return super.getFieldValue(obj.data, key, isDate, isTime);
×
385
    }
386
}
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