Skip to content

Commit 6048fb2

Browse files
feat(girds): add custom data clone strategy for batch editing #10678 (#10734)
1 parent b20aedd commit 6048fb2

26 files changed

+228
-43
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes for each version of this project will be documented in this file.
44

5+
## 13.0.5
6+
7+
### New Features
8+
- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`
9+
- Added `dataCloneStrategy` input, which allows users provide their own implementation of how data objects are cloned when row and/or batch editing is enabled. The custom strategy should implement the `IDataCloneStrategy` interface.
10+
511
## 13.0.1
612

713
### New Features

projects/igniteui-angular/src/lib/core/utils.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ export const cloneHierarchicalArray = (array: any[], childDataKey: any): any[] =
8181
return result;
8282
};
8383

84+
/**
85+
* Creates an object with prototype from provided source and copies
86+
* all properties descriptors from provided source
87+
* @param obj Source to copy prototype and descriptors from
88+
* @returns New object with cloned prototype and property descriptors
89+
*/
90+
export const copyDescriptors = (obj) => {
91+
if (obj) {
92+
return Object.create(
93+
Object.getPrototypeOf(obj),
94+
Object.getOwnPropertyDescriptors(obj)
95+
);
96+
}
97+
}
98+
99+
84100
/**
85101
* Deep clones all first level keys of Obj2 and merges them to Obj1
86102
*
@@ -164,7 +180,7 @@ export const uniqueDates = (columnValues: any[]) => columnValues.reduce((a, c) =
164180
* @returns true if provided variable is Object
165181
* @hidden
166182
*/
167-
export const isObject = (value: any): boolean => value && value.toString() === '[object Object]';
183+
export const isObject = (value: any): boolean => !!(value && value.toString() === '[object Object]');
168184

169185
/**
170186
* Checks if provided variable is Date
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { cloneValue } from "../core/utils";
2+
3+
export interface IDataCloneStrategy {
4+
clone(data: any): any;
5+
}
6+
7+
export class DefaultDataCloneStrategy implements IDataCloneStrategy {
8+
public clone(data: any): any {
9+
return cloneValue(data);
10+
}
11+
}

projects/igniteui-angular/src/lib/data-operations/data-util.spec.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { IPagingState, PagingError } from './paging-state.interface';
2121
import { SampleTestData } from '../test-utils/sample-test-data.spec';
2222
import { Transaction, TransactionType, HierarchicalTransaction } from '../services/public_api';
23+
import { DefaultDataCloneStrategy } from './data-clone-strategy';
2324

2425
/* Test sorting */
2526
const testSort = () => {
@@ -516,19 +517,21 @@ const testMerging = () => {
516517
});
517518

518519
it('Should merge delete transactions correctly', () => {
520+
const cloneStrategy = new DefaultDataCloneStrategy();
519521
const data = SampleTestData.personIDNameData();
520522
const secondRow = data[1];
521523
const transactions: Transaction[] = [
522524
{ id: 1, newValue: null, type: TransactionType.DELETE },
523525
{ id: 3, newValue: null, type: TransactionType.DELETE },
524526
];
525527

526-
DataUtil.mergeTransactions(data, transactions, 'ID', true);
528+
DataUtil.mergeTransactions(data, transactions, 'ID', cloneStrategy, true);
527529
expect(data.length).toBe(1);
528530
expect(data[0]).toEqual(secondRow);
529531
});
530532

531533
it('Should merge add hierarchical transactions correctly', () => {
534+
const cloneStrategy = new DefaultDataCloneStrategy();
532535
const data = SampleTestData.employeeSmallTreeData();
533536
const addRootRow = { ID: 1000, Name: 'Pit Peter', HireDate: new Date(2008, 3, 20), Age: 55 };
534537
const addChildRow1 = { ID: 1001, Name: 'Marry May', HireDate: new Date(2018, 4, 1), Age: 102 };
@@ -539,7 +542,7 @@ const testMerging = () => {
539542
{ id: addChildRow2.ID, newValue: addChildRow2, type: TransactionType.ADD, path: [addRootRow.ID] },
540543
];
541544

542-
DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', false);
545+
DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', cloneStrategy, false);
543546
expect(data.length).toBe(4);
544547

545548
expect(data[3].Age).toBe(addRootRow.Age);
@@ -555,6 +558,7 @@ const testMerging = () => {
555558
});
556559

557560
it('Should merge update hierarchical transactions correctly', () => {
561+
const cloneStrategy = new DefaultDataCloneStrategy();
558562
const data = SampleTestData.employeeSmallTreeData();
559563
const updateRootRow = { Name: 'May Peter', Age: 13 };
560564
const updateChildRow1 = { HireDate: new Date(2100, 1, 12), Age: 1300 };
@@ -581,7 +585,7 @@ const testMerging = () => {
581585
},
582586
];
583587

584-
DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', false);
588+
DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID',cloneStrategy, false);
585589
expect(data[1].Name).toBe(updateRootRow.Name);
586590
expect(data[1].Age).toBe(updateRootRow.Age);
587591

@@ -593,6 +597,7 @@ const testMerging = () => {
593597
});
594598

595599
it('Should merge delete hierarchical transactions correctly', () => {
600+
const cloneStrategy = new DefaultDataCloneStrategy();
596601
const data = SampleTestData.employeeSmallTreeData();
597602
const transactions: HierarchicalTransaction[] = [
598603
// root row with no children
@@ -605,7 +610,7 @@ const testMerging = () => {
605610
{ id: data[0].Employees[2].ID, newValue: null, type: TransactionType.DELETE, path: [data[0].ID] }
606611
];
607612

608-
DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', true);
613+
DataUtil.mergeHierarchicalTransactions(data, transactions, 'Employees', 'ID', cloneStrategy, true);
609614

610615
expect(data.length).toBe(1);
611616
expect(data[0].Employees.length).toBe(1);

projects/igniteui-angular/src/lib/data-operations/data-util.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IGroupByKey } from './groupby-expand-state.interface';
88
import { IGroupByRecord } from './groupby-record.interface';
99
import { IGroupingState } from './groupby-state.interface';
1010
import { FilteringStrategy } from './filtering-strategy';
11-
import { cloneValue, mergeObjects, mkenum } from '../core/utils';
11+
import { mergeObjects, mkenum } from '../core/utils';
1212
import { Transaction, TransactionType, HierarchicalTransaction } from '../services/transaction/transaction';
1313
import { getHierarchy, isHierarchyMatch } from './operations';
1414
import { GridType } from '../grids/common/grid.interface';
@@ -21,6 +21,7 @@ import {
2121
IgxSorting,
2222
IgxGrouping
2323
} from '../grids/common/strategy';
24+
import { DefaultDataCloneStrategy, IDataCloneStrategy } from '../data-operations/data-clone-strategy';
2425

2526
/**
2627
* @hidden
@@ -147,12 +148,12 @@ export class DataUtil {
147148
* @param deleteRows Should delete rows with DELETE transaction type from data
148149
* @returns Provided data collections updated with all provided transactions
149150
*/
150-
public static mergeTransactions<T>(data: T[], transactions: Transaction[], primaryKey?: any, deleteRows: boolean = false): T[] {
151+
public static mergeTransactions<T>(data: T[], transactions: Transaction[], primaryKey?: any, cloneStrategy: IDataCloneStrategy = new DefaultDataCloneStrategy(), deleteRows: boolean = false): T[] {
151152
data.forEach((item: any, index: number) => {
152153
const rowId = primaryKey ? item[primaryKey] : item;
153154
const transaction = transactions.find(t => t.id === rowId);
154155
if (transaction && transaction.type === TransactionType.UPDATE) {
155-
data[index] = transaction.newValue;
156+
data[index] = mergeObjects(cloneStrategy.clone(data[index]), transaction.newValue);
156157
}
157158
});
158159

@@ -189,6 +190,7 @@ export class DataUtil {
189190
transactions: HierarchicalTransaction[],
190191
childDataKey: any,
191192
primaryKey?: any,
193+
cloneStrategy: IDataCloneStrategy = new DefaultDataCloneStrategy(),
192194
deleteRows: boolean = false): any[] {
193195
for (const transaction of transactions) {
194196
if (transaction.path) {
@@ -205,7 +207,7 @@ export class DataUtil {
205207
case TransactionType.UPDATE:
206208
const updateIndex = collection.findIndex(x => x[primaryKey] === transaction.id);
207209
if (updateIndex !== -1) {
208-
collection[updateIndex] = mergeObjects(cloneValue(collection[updateIndex]), transaction.newValue);
210+
collection[updateIndex] = mergeObjects(cloneStrategy.clone(collection[updateIndex]), transaction.newValue);
209211
}
210212
break;
211213
case TransactionType.DELETE:

projects/igniteui-angular/src/lib/grids/api.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ export class GridBaseAPIService<T extends GridType> implements GridServiceType {
4040
data = DataUtil.mergeTransactions(
4141
cloneArray(grid.data),
4242
grid.transactions.getAggregatedChanges(true),
43-
grid.primaryKey
43+
grid.primaryKey,
44+
grid.dataCloneStrategy
4445
);
4546
const deletedRows = grid.transactions.getTransactionLog().filter(t => t.type === TransactionType.DELETE).map(t => t.id);
4647
deletedRows.forEach(rowID => {

projects/igniteui-angular/src/lib/grids/common/crud.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { first } from 'rxjs/operators';
33
import { IGridEditDoneEventArgs, IGridEditEventArgs, IRowDataEventArgs } from '../common/events';
44
import { GridType, RowType } from './grid.interface';
55
import { Subject } from 'rxjs';
6-
import { isEqual } from '../../core/utils';
6+
import { copyDescriptors, isEqual } from '../../core/utils';
77

88
export class IgxEditRow {
99
public transactionState: any;
@@ -405,7 +405,8 @@ export class IgxRowCrudState extends IgxCellCrudState {
405405

406406

407407
if (rowInEditMode && row.id === rowInEditMode.id) {
408-
row.data = { ...row.data, ...rowInEditMode.transactionState };
408+
// do not use spread operator here as it will copy everything over an empty object with no descriptors
409+
row.data = Object.assign(copyDescriptors(row.data), row.data, rowInEditMode.transactionState);
409410
// TODO: Workaround for updating a row in edit mode through the API
410411
} else if (this.grid.transactions.enabled) {
411412
const state = grid.transactions.getState(row.id);

projects/igniteui-angular/src/lib/grids/common/grid.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { IGridGroupingStrategy, IGridSortingStrategy } from './strategy';
3434
import { IForOfState, IgxGridForOfDirective } from '../../directives/for-of/for_of.directive';
3535
import { OverlaySettings } from '../../services/overlay/utilities';
3636
import { IPinningConfig } from '../grid.common';
37+
import { IDataCloneStrategy } from '../../data-operations/data-clone-strategy';
3738

3839
export const IGX_GRID_BASE = new InjectionToken<GridType>('IgxGridBaseToken');
3940
export const IGX_GRID_SERVICE_BASE = new InjectionToken<GridServiceType>('IgxGridServiceBaseToken');
@@ -267,6 +268,7 @@ export interface GridType extends IGridDataBindable {
267268
summaryPipeTrigger: number;
268269
hasColumnLayouts: boolean;
269270
isLoading: boolean;
271+
dataCloneStrategy: IDataCloneStrategy;
270272

271273
gridAPI: GridServiceType;
272274

projects/igniteui-angular/src/lib/grids/common/pipes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ export class IgxGridTransactionPipe implements PipeTransform {
200200
const result = DataUtil.mergeTransactions(
201201
cloneArray(collection),
202202
this.grid.transactions.getAggregatedChanges(true),
203-
this.grid.primaryKey);
203+
this.grid.primaryKey,
204+
this.grid.dataCloneStrategy);
204205
return result;
205206
}
206207
return collection;

projects/igniteui-angular/src/lib/grids/grid-base.directive.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ import { IGridSortingStrategy } from './common/strategy';
150150
import { IgxGridExcelStyleFilteringComponent } from './filtering/excel-style/grid.excel-style-filtering.component';
151151
import { IgxGridHeaderComponent } from './headers/grid-header.component';
152152
import { IgxGridFilteringRowComponent } from './filtering/base/grid-filtering-row.component';
153+
import { DefaultDataCloneStrategy, IDataCloneStrategy } from '../data-operations/data-clone-strategy';
153154

154155
let FAKE_ROW_ID = -1;
155156
const DEFAULT_ITEMS_PER_PAGE = 15;
@@ -240,6 +241,26 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
240241
return 0;
241242
}
242243

244+
/**
245+
* Gets/Sets the data clone strategy of the grid when in edit mode.
246+
*
247+
* @example
248+
* ```html
249+
* <igx-grid #grid [data]="localData" [dataCloneStrategy]="customCloneStrategy"></igx-grid>
250+
* ```
251+
*/
252+
@Input()
253+
public get dataCloneStrategy(): IDataCloneStrategy {
254+
return this._dataCloneStrategy;
255+
}
256+
257+
public set dataCloneStrategy(strategy: IDataCloneStrategy) {
258+
if (strategy) {
259+
this._dataCloneStrategy = strategy;
260+
this._transactions.cloneStrategy = strategy;
261+
}
262+
}
263+
243264
/**
244265
* Controls the copy behavior of the grid.
245266
*/
@@ -2796,6 +2817,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
27962817
private transactionChange$ = new Subject<void>();
27972818
private _rendered = false;
27982819
private readonly DRAG_SCROLL_DELTA = 10;
2820+
private _dataCloneStrategy: IDataCloneStrategy = new DefaultDataCloneStrategy();
27992821

28002822
/**
28012823
* @hidden @internal
@@ -2951,6 +2973,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
29512973
super(_displayDensityOptions);
29522974
this.locale = this.locale || this.localeId;
29532975
this._transactions = this.transactionFactory.create(TRANSACTION_TYPE.None);
2976+
this._transactions.cloneStrategy = this.dataCloneStrategy;
29542977
this.cdr.detach();
29552978
}
29562979

@@ -5923,6 +5946,10 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
59235946
} else {
59245947
this._transactions = this.transactionFactory.create(TRANSACTION_TYPE.None);
59255948
}
5949+
5950+
if (this.dataCloneStrategy) {
5951+
this._transactions.cloneStrategy = this.dataCloneStrategy;
5952+
}
59265953
}
59275954

59285955
protected subscribeToTransactions(): void {

0 commit comments

Comments
 (0)