diff --git a/CHANGELOG.md b/CHANGELOG.md index bb4e199c32c..bdef1bd56c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes for each version of this project will be documented in this ## 6.1.8 +### General + +- `sortStrategy` input exposed to provide custom sort strategy for the `IgxColumnComponent`. The custom strategy should implement the `ISortingStrategy` interface, or can extend the base `SortingStrategy` class and override all or some of its public/protected members. + ### Bug fixes - Fix sorting and groupby expression not syncing when there are already sorted columns. #2786 diff --git a/projects/igniteui-angular/src/lib/data-operations/sorting-expression.interface.ts b/projects/igniteui-angular/src/lib/data-operations/sorting-expression.interface.ts index 8d562b93546..58d9b7f0543 100644 --- a/projects/igniteui-angular/src/lib/data-operations/sorting-expression.interface.ts +++ b/projects/igniteui-angular/src/lib/data-operations/sorting-expression.interface.ts @@ -1,3 +1,5 @@ +import { ISortingStrategy } from './sorting-strategy'; + /** * Represents sorting expressions. */ @@ -11,4 +13,5 @@ export interface ISortingExpression { fieldName: string; dir: SortingDirection; ignoreCase?: boolean; + strategy?: ISortingStrategy; } diff --git a/projects/igniteui-angular/src/lib/data-operations/sorting-strategy.ts b/projects/igniteui-angular/src/lib/data-operations/sorting-strategy.ts index 1a5553a6840..0f84d18050b 100644 --- a/projects/igniteui-angular/src/lib/data-operations/sorting-strategy.ts +++ b/projects/igniteui-angular/src/lib/data-operations/sorting-strategy.ts @@ -38,14 +38,18 @@ export class SortingStrategy implements ISortingStrategy { } return a > b ? 1 : a < b ? -1 : 0; } - protected compareObjects(obj1: object, obj2: object, key: string, reverse: number, ignoreCase: boolean) { + protected compareObjects(obj1: object, obj2: object, key: string, reverse: number, ignoreCase: boolean, strategy: ISortingStrategy) { let a = obj1[key]; let b = obj2[key]; if (ignoreCase) { a = a && a.toLowerCase ? a.toLowerCase() : a; b = b && b.toLowerCase ? b.toLowerCase() : b; } - return reverse * this.compareValues(a, b); + if (strategy) { + return reverse * strategy.compareValues(a, b); + } else { + return reverse * this.compareValues(a, b); + } } protected arraySort(data: T[], compareFn?): T[] { return data.sort(compareFn); @@ -78,7 +82,7 @@ export class SortingStrategy implements ISortingStrategy { false; const reverse = (expression.dir === SortingDirection.Desc ? -1 : 1); const cmpFunc = (obj1, obj2) => { - return this.compareObjects(obj1, obj2, key, reverse, ignoreCase); + return this.compareObjects(obj1, obj2, key, reverse, ignoreCase, expression.strategy); }; return this.arraySort(data, cmpFunc); } diff --git a/projects/igniteui-angular/src/lib/grid/api.service.ts b/projects/igniteui-angular/src/lib/grid/api.service.ts index fd19722de9b..aa5415b69b4 100644 --- a/projects/igniteui-angular/src/lib/grid/api.service.ts +++ b/projects/igniteui-angular/src/lib/grid/api.service.ts @@ -11,6 +11,8 @@ import { IgxColumnComponent } from './column.component'; import { IGridEditEventArgs, IgxGridComponent } from './grid.component'; import { IgxGridRowComponent } from './row.component'; import { IFilteringOperation, FilteringExpressionsTree, IFilteringExpressionsTree } from '../../public_api'; +import { ISortingStrategy } from '../data-operations/sorting-strategy'; +import { SortingStateDefaults } from '../data-operations/sorting-state.interface'; /** *@hidden */ @@ -207,13 +209,13 @@ export class IgxGridAPIService { } } - public sort(id: string, fieldName: string, dir: SortingDirection, ignoreCase: boolean): void { + public sort(id: string, fieldName: string, dir: SortingDirection, ignoreCase: boolean, strategy: ISortingStrategy): void { if (dir === SortingDirection.None) { this.remove_grouping_expression(id, fieldName); } const sortingState = cloneArray(this.get(id).sortingExpressions); - - this.prepare_sorting_expression([sortingState], { fieldName, dir, ignoreCase }); + strategy = strategy ? strategy : this.getSortStrategyPerColumn(id, fieldName); + this.prepare_sorting_expression([sortingState], { fieldName, dir, ignoreCase, strategy }); this.get(id).sortingExpressions = sortingState; } @@ -224,17 +226,18 @@ export class IgxGridAPIService { if (each.dir === SortingDirection.None) { this.remove_grouping_expression(id, each.fieldName); } + each.strategy = each.strategy ? each.strategy : this.getSortStrategyPerColumn(id, each.fieldName); this.prepare_sorting_expression([sortingState], each); } this.get(id).sortingExpressions = sortingState; } - public groupBy(id: string, fieldName: string, dir: SortingDirection, ignoreCase: boolean): void { + public groupBy(id: string, fieldName: string, dir: SortingDirection, ignoreCase: boolean, strategy: ISortingStrategy): void { const groupingState = cloneArray(this.get(id).groupingExpressions); const sortingState = cloneArray(this.get(id).sortingExpressions); - - this.prepare_sorting_expression([sortingState, groupingState], { fieldName, dir, ignoreCase }); + strategy = strategy ? strategy : this.getSortStrategyPerColumn(id, fieldName); + this.prepare_sorting_expression([sortingState, groupingState], { fieldName, dir, ignoreCase, strategy }); this.get(id).groupingExpressions = groupingState; this.arrange_sorting_expressions(id); } @@ -244,6 +247,7 @@ export class IgxGridAPIService { const sortingState = cloneArray(this.get(id).sortingExpressions); for (const each of expressions) { + each.strategy = each.strategy ? each.strategy : this.getSortStrategyPerColumn(id, each.fieldName); this.prepare_sorting_expression([sortingState, groupingState], each); } @@ -476,4 +480,9 @@ export class IgxGridAPIService { groupingExpressions.splice(index, 1); } } + + protected getSortStrategyPerColumn(id: string, fieldName: string) { + return this.get_column_by_name(this.get(id).id, fieldName) ? + this.get_column_by_name(id, fieldName).sortStrategy : undefined; + } } diff --git a/projects/igniteui-angular/src/lib/grid/column.component.ts b/projects/igniteui-angular/src/lib/grid/column.component.ts index b427c4456d2..e94baeb0926 100644 --- a/projects/igniteui-angular/src/lib/grid/column.component.ts +++ b/projects/igniteui-angular/src/lib/grid/column.component.ts @@ -30,6 +30,7 @@ import { } from '../../public_api'; import { IgxGridHeaderComponent } from './grid-header.component'; import { valToPxlsUsingRange } from '../core/utils'; +import { SortingStrategy } from '../data-operations/sorting-strategy'; /** * **Ignite UI for Angular Column** - @@ -468,6 +469,31 @@ export class IgxColumnComponent implements AfterContentInit { public set filters(classRef: any) { this._filters = classRef; } + /** + * Gets the column `sortStrategy`. + * ```typescript + * let sortStrategy = this.column.sortStrategy' + * ``` + * @memberof IgxColumnComponent + */ + @Input() + public get sortStrategy(): any { + return this._sortStrategy; + } + /** + * Sets the column `sortStrategy`. + * ```typescript + * this.column.sortStrategy = new CustomSortingStrategy(). + * + * class CustomSortingStrategy extends SortingStrategy { + * ... + * } + * ``` + * @memberof IgxColumnComponent + */ + public set sortStrategy(classRef: any) { + this._sortStrategy = classRef; + } /** * Gets the default minimum `width` of the column. * ```typescript @@ -715,6 +741,10 @@ export class IgxColumnComponent implements AfterContentInit { *@hidden */ protected _filters = null; + /** + *@hidden + */ + protected _sortStrategy = new SortingStrategy(); /** *@hidden */ diff --git a/projects/igniteui-angular/src/lib/grid/grid-header.component.ts b/projects/igniteui-angular/src/lib/grid/grid-header.component.ts index fa5020bf225..41d0d55f049 100644 --- a/projects/igniteui-angular/src/lib/grid/grid-header.component.ts +++ b/projects/igniteui-angular/src/lib/grid/grid-header.component.ts @@ -179,11 +179,13 @@ export class IgxGridHeaderComponent implements OnInit, DoCheck, AfterViewInit { this.sortDirection + 1 > SortingDirection.Desc ? SortingDirection.Asc : SortingDirection.Desc : this.sortDirection + 1 > SortingDirection.Desc ? SortingDirection.None : this.sortDirection + 1; this.sortDirection = sortDir; - this.grid.sort({ fieldName: this.column.field, dir: this.sortDirection, ignoreCase: this.column.sortingIgnoreCase }); + this.grid.sort({ fieldName: this.column.field, dir: this.sortDirection, ignoreCase: this.column.sortingIgnoreCase, + strategy: this.column.sortStrategy }); this.grid.onSortingDone.emit({ dir: this.sortDirection, fieldName: this.column.field, - ignoreCase: this.column.sortingIgnoreCase + ignoreCase: this.column.sortingIgnoreCase, + strategy: this.column.sortStrategy }); } } diff --git a/projects/igniteui-angular/src/lib/grid/grid.common.ts b/projects/igniteui-angular/src/lib/grid/grid.common.ts index 9d1b4e8c219..643476d7363 100644 --- a/projects/igniteui-angular/src/lib/grid/grid.common.ts +++ b/projects/igniteui-angular/src/lib/grid/grid.common.ts @@ -576,7 +576,8 @@ export class IgxGroupAreaDropDirective extends IgxDropDirective { const column: IgxColumnComponent = drag.column; const isGrouped = column.grid.groupingExpressions.findIndex((item) => item.fieldName === column.field) !== -1; if (column.groupable && !isGrouped) { - column.grid.groupBy({ fieldName: column.field, dir: SortingDirection.Asc, ignoreCase: column.sortingIgnoreCase }); + column.grid.groupBy({ fieldName: column.field, dir: SortingDirection.Asc, ignoreCase: column.sortingIgnoreCase, + strategy: column.sortStrategy }); } } } diff --git a/projects/igniteui-angular/src/lib/grid/grid.component.ts b/projects/igniteui-angular/src/lib/grid/grid.component.ts index 0d45cb9dcdf..d7dc372b433 100644 --- a/projects/igniteui-angular/src/lib/grid/grid.component.ts +++ b/projects/igniteui-angular/src/lib/grid/grid.component.ts @@ -3423,7 +3423,7 @@ export class IgxGridComponent implements OnInit, OnDestroy, AfterContentInit, Af * @hidden */ protected _sort(expression: ISortingExpression) { - this.gridAPI.sort(this.id, expression.fieldName, expression.dir, expression.ignoreCase); + this.gridAPI.sort(this.id, expression.fieldName, expression.dir, expression.ignoreCase, expression.strategy); } /** @@ -3437,7 +3437,7 @@ export class IgxGridComponent implements OnInit, OnDestroy, AfterContentInit, Af * @hidden */ protected _groupBy(expression: ISortingExpression) { - this.gridAPI.groupBy(this.id, expression.fieldName, expression.dir, expression.ignoreCase); + this.gridAPI.groupBy(this.id, expression.fieldName, expression.dir, expression.ignoreCase, expression.strategy); } /** diff --git a/projects/igniteui-angular/src/lib/grid/grid.groupby.spec.ts b/projects/igniteui-angular/src/lib/grid/grid.groupby.spec.ts index a11091f92e9..b597e608e4b 100644 --- a/projects/igniteui-angular/src/lib/grid/grid.groupby.spec.ts +++ b/projects/igniteui-angular/src/lib/grid/grid.groupby.spec.ts @@ -13,6 +13,8 @@ import { IgxGridRowComponent } from './row.component'; import { IgxChipComponent } from '../chips/chip.component'; import { wait, UIInteractions } from '../test-utils/ui-interactions.spec'; import { HelperUtils} from '../test-utils/helper-utils.spec'; +import { SortingStrategy } from '../data-operations/sorting-strategy'; +import { SortingStateDefaults } from '../data-operations/sorting-state.interface'; describe('IgxGrid - GroupBy', () => { const COLUMN_HEADER_CLASS = '.igx-grid__th'; @@ -1916,24 +1918,27 @@ describe('IgxGrid - GroupBy', () => { it('should update grouping expression when sorting a column first then grouping by it and changing sorting for it again', () => { const fix = TestBed.createComponent(DefaultGridComponent); const grid = fix.componentInstance.instance; + let strategy = new CustomSortingStrategy(); fix.componentInstance.enableSorting = true; fix.detectChanges(); - grid.sort({ fieldName: 'ID', dir: SortingDirection.Asc, ignoreCase: false }); + grid.sort({ fieldName: 'ID', dir: SortingDirection.Asc, ignoreCase: false, strategy: new CustomSortingStrategy() }); - expect(grid.sortingExpressions).toEqual([{ fieldName: 'ID', dir: SortingDirection.Asc, ignoreCase: false }]); + expect(grid.sortingExpressions).toEqual([{ fieldName: 'ID', dir: SortingDirection.Asc, ignoreCase: false, strategy: strategy }]); expect(grid.groupingExpressions).toEqual([]); + strategy = SortingStateDefaults.strategy; grid.groupBy({ fieldName: 'ID', dir: SortingDirection.Asc, ignoreCase: false }); grid.sort({ fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false }); - expect(grid.sortingExpressions).toEqual([{ fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false }]); - expect(grid.groupingExpressions).toEqual([{ fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false }]); + expect(grid.sortingExpressions).toEqual([{ fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false, strategy: strategy }]); + expect(grid.groupingExpressions).toEqual([{ fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false, strategy: strategy }]); }); it('should update grouping expression when sorting a column first then grouping by another and changing sorting for it', () => { const fix = TestBed.createComponent(DefaultGridComponent); const grid = fix.componentInstance.instance; + const strategy = SortingStateDefaults.strategy; fix.componentInstance.enableSorting = true; fix.detectChanges(); @@ -1941,8 +1946,8 @@ describe('IgxGrid - GroupBy', () => { grid.sort({ fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false }); expect(grid.sortingExpressions).toEqual([ - { fieldName: 'Downloads', dir: SortingDirection.Asc, ignoreCase: false }, - { fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false } + { fieldName: 'Downloads', dir: SortingDirection.Asc, ignoreCase: false, strategy: strategy }, + { fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false, strategy: strategy } ]); expect(grid.groupingExpressions).toEqual([]); @@ -1950,11 +1955,12 @@ describe('IgxGrid - GroupBy', () => { grid.sort({ fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false }); expect(grid.sortingExpressions).toEqual([ - { fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false }, - { fieldName: 'Downloads', dir: SortingDirection.Asc, ignoreCase: false }, - { fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false } + { fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false, strategy: strategy }, + { fieldName: 'Downloads', dir: SortingDirection.Asc, ignoreCase: false, strategy: strategy }, + { fieldName: 'ID', dir: SortingDirection.Desc, ignoreCase: false, strategy: strategy } ]); - expect(grid.groupingExpressions).toEqual([{ fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false }]); + expect(grid.groupingExpressions).toEqual([{ fieldName: 'Released', dir: SortingDirection.Desc, ignoreCase: false, + strategy: strategy }]); }); function sendInput(element, text, fix) { @@ -2155,3 +2161,6 @@ export class GroupByDataMoreColumnsComponent extends DataParent { @ViewChild(IgxGridComponent, { read: IgxGridComponent }) public instance: IgxGridComponent; } + +export class CustomSortingStrategy extends SortingStrategy { +} diff --git a/projects/igniteui-angular/src/lib/grid/grid.pipes.ts b/projects/igniteui-angular/src/lib/grid/grid.pipes.ts index 4ab3c6c0ccb..76b91417cfd 100644 --- a/projects/igniteui-angular/src/lib/grid/grid.pipes.ts +++ b/projects/igniteui-angular/src/lib/grid/grid.pipes.ts @@ -3,7 +3,7 @@ import { cloneArray } from '../core/utils'; import { DataUtil } from '../data-operations/data-util'; import { FilteringLogic, IFilteringExpression } from '../data-operations/filtering-expression.interface'; import { IGroupByExpandState } from '../data-operations/groupby-expand-state.interface'; -import { IGroupByResult } from '../data-operations/sorting-strategy'; +import { IGroupByResult, ISortingStrategy } from '../data-operations/sorting-strategy'; import { IFilteringExpressionsTree } from '../data-operations/filtering-expressions-tree'; import { ISortingExpression } from '../data-operations/sorting-expression.interface'; import { IgxGridAPIService } from './api.service'; @@ -22,14 +22,19 @@ export class IgxGridSortingPipe implements PipeTransform { public transform(collection: any[], expressions: ISortingExpression | ISortingExpression[], id: string, pipeTrigger: number): any[] { - - const state = { expressions: [] }; + let strategy: ISortingStrategy; + const state = { expressions: [], strategy }; state.expressions = this.gridAPI.get(id).sortingExpressions; if (!state.expressions.length) { return collection; } + // DataUtil.sort needs a sorting strategy to start with, so it makes sense to start with the strategy from the first expression + // sorting-strategy.ts, sortDataRecursive method then takes care and use the corresponding strategy for each expression + strategy = expressions[0].strategy; + state.strategy = strategy; + return DataUtil.sort(cloneArray(collection), state); } }