Skip to content

Commit ea78a7a

Browse files
authored
Merge branch 'master' into rkaraivanov/column-moving-grid-prop
2 parents db09bcd + 826242c commit ea78a7a

33 files changed

+446
-75
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ All notable changes for each version of this project will be documented in this
1515
</igx-grid>
1616
```
1717

18+
## 13.0.5
19+
20+
### New Features
21+
- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`
22+
- 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.
23+
1824
## 13.0.1
1925

2026
### New Features

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

+17-1
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
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

+9-4
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

+6-4
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

+2-1
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

+3-2
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

+2
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');
@@ -269,6 +270,7 @@ export interface GridType extends IGridDataBindable {
269270
hasColumnLayouts: boolean;
270271
moving: boolean;
271272
isLoading: boolean;
273+
dataCloneStrategy: IDataCloneStrategy;
272274

273275
gridAPI: GridServiceType;
274276

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

+2-1
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

+27
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;
@@ -247,6 +248,26 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
247248
return 0;
248249
}
249250

251+
/**
252+
* Gets/Sets the data clone strategy of the grid when in edit mode.
253+
*
254+
* @example
255+
* ```html
256+
* <igx-grid #grid [data]="localData" [dataCloneStrategy]="customCloneStrategy"></igx-grid>
257+
* ```
258+
*/
259+
@Input()
260+
public get dataCloneStrategy(): IDataCloneStrategy {
261+
return this._dataCloneStrategy;
262+
}
263+
264+
public set dataCloneStrategy(strategy: IDataCloneStrategy) {
265+
if (strategy) {
266+
this._dataCloneStrategy = strategy;
267+
this._transactions.cloneStrategy = strategy;
268+
}
269+
}
270+
250271
/**
251272
* Controls the copy behavior of the grid.
252273
*/
@@ -2803,6 +2824,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
28032824
private transactionChange$ = new Subject<void>();
28042825
private _rendered = false;
28052826
private readonly DRAG_SCROLL_DELTA = 10;
2827+
private _dataCloneStrategy: IDataCloneStrategy = new DefaultDataCloneStrategy();
28062828

28072829
/**
28082830
* @hidden @internal
@@ -2958,6 +2980,7 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
29582980
super(_displayDensityOptions);
29592981
this.locale = this.locale || this.localeId;
29602982
this._transactions = this.transactionFactory.create(TRANSACTION_TYPE.None);
2983+
this._transactions.cloneStrategy = this.dataCloneStrategy;
29612984
this.cdr.detach();
29622985
}
29632986

@@ -5933,6 +5956,10 @@ export abstract class IgxGridBaseDirective extends DisplayDensityBase implements
59335956
} else {
59345957
this._transactions = this.transactionFactory.create(TRANSACTION_TYPE.None);
59355958
}
5959+
5960+
if (this.dataCloneStrategy) {
5961+
this._transactions.cloneStrategy = this.dataCloneStrategy;
5962+
}
59365963
}
59375964

59385965
protected subscribeToTransactions(): void {

projects/igniteui-angular/src/lib/grids/grid-public-row.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { IgxGridCell } from './grid-public-cell';
55
import { IgxSummaryResult } from './summaries/grid-summary';
66
import { ITreeGridRecord } from './tree-grid/tree-grid.interfaces';
77
import mergeWith from 'lodash.mergewith';
8-
import { cloneValue } from '../core/utils';
98
import { CellType, GridServiceType, GridType, RowType } from './common/grid.interface';
109

1110
abstract class BaseRow implements RowType {
@@ -48,7 +47,7 @@ abstract class BaseRow implements RowType {
4847
*/
4948
public get data(): any {
5049
if (this.inEditMode) {
51-
return mergeWith(cloneValue(this._data ?? this.grid.dataView[this.index]),
50+
return mergeWith(this.grid.dataCloneStrategy.clone(this._data ?? this.grid.dataView[this.index]),
5251
this.grid.transactions.getAggregatedValue(this.key, false),
5352
(objValue, srcValue) => {
5453
if (Array.isArray(srcValue)) {
@@ -352,7 +351,7 @@ export class IgxTreeGridRow extends BaseRow implements RowType {
352351
*/
353352
public get data(): any {
354353
if (this.inEditMode) {
355-
return mergeWith(cloneValue(this._data ?? this.grid.dataView[this.index]),
354+
return mergeWith(this.grid.dataCloneStrategy.clone(this._data ?? this.grid.dataView[this.index]),
356355
this.grid.transactions.getAggregatedValue(this.key, false),
357356
(objValue, srcValue) => {
358357
if (Array.isArray(srcValue)) {

0 commit comments

Comments
 (0)