Skip to content

Commit 8b1054b

Browse files
authored
Merge branch '19.2.x' into ikitanov/fix-15839-19.2.x
2 parents 6623e9e + b3f28e4 commit 8b1054b

File tree

11 files changed

+179
-40
lines changed

11 files changed

+179
-40
lines changed

projects/igniteui-angular/src/lib/grids/cell.component.html

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -226,37 +226,38 @@
226226
}
227227

228228
<ng-template #defaultError>
229-
@if (formGroup?.get(column?.field).errors?.['required']) {
229+
@let errors = formControl.errors;
230+
@if (errors?.['required']) {
230231
<div>
231232
{{grid.resourceStrings.igx_grid_required_validation_error}}
232233
</div>
233234
}
234-
@if (formGroup?.get(column?.field).errors?.['minlength']) {
235+
@if (errors?.['minlength']) {
235236
<div>
236-
{{grid.resourceStrings.igx_grid_min_length_validation_error | igxStringReplace:'{0}':formGroup.get(column.field).errors.minlength.requiredLength }}
237+
{{grid.resourceStrings.igx_grid_min_length_validation_error | igxStringReplace:'{0}':errors.minlength.requiredLength }}
237238
</div>
238239
}
239-
@if (formGroup?.get(column?.field).errors?.['maxlength']) {
240+
@if (errors?.['maxlength']) {
240241
<div>
241-
{{grid.resourceStrings.igx_grid_max_length_validation_error | igxStringReplace:'{0}':formGroup.get(column.field).errors.maxlength.requiredLength }}
242+
{{grid.resourceStrings.igx_grid_max_length_validation_error | igxStringReplace:'{0}':errors.maxlength.requiredLength }}
242243
</div>
243244
}
244-
@if (formGroup?.get(column?.field).errors?.['min']) {
245+
@if (errors?.['min']) {
245246
<div>
246-
{{grid.resourceStrings.igx_grid_min_validation_error | igxStringReplace:'{0}':formGroup.get(column.field).errors.min.min }}
247+
{{grid.resourceStrings.igx_grid_min_validation_error | igxStringReplace:'{0}':errors.min.min }}
247248
</div>
248249
}
249-
@if (formGroup?.get(column?.field).errors?.['max']) {
250+
@if (errors?.['max']) {
250251
<div>
251-
{{grid.resourceStrings.igx_grid_max_validation_error | igxStringReplace:'{0}':formGroup.get(column.field).errors.max.max }}
252+
{{grid.resourceStrings.igx_grid_max_validation_error | igxStringReplace:'{0}':errors.max.max }}
252253
</div>
253254
}
254-
@if (formGroup?.get(column?.field).errors?.['email']) {
255+
@if (errors?.['email']) {
255256
<div>
256257
{{grid.resourceStrings.igx_grid_email_validation_error }}
257258
</div>
258259
}
259-
@if (formGroup?.get(column?.field).errors?.['pattern']) {
260+
@if (errors?.['pattern']) {
260261
<div>
261262
{{grid.resourceStrings.igx_grid_pattern_validation_error}}
262263
</div>

projects/igniteui-angular/src/lib/grids/cell.component.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -538,8 +538,11 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
538538
@HostBinding('class.igx-grid__td--invalid')
539539
@HostBinding('attr.aria-invalid')
540540
public get isInvalid() {
541-
const isInvalid = this.formGroup?.get(this.column?.field)?.invalid && this.formGroup?.get(this.column?.field)?.touched;
542-
return !this.intRow.deleted && isInvalid;
541+
if (this.formGroup) {
542+
const isInvalid = this.grid.validation?.isFieldInvalid(this.formGroup, this.column?.field);
543+
return !this.intRow.deleted && isInvalid;
544+
}
545+
return false;
543546
}
544547

545548
/**
@@ -548,8 +551,11 @@ export class IgxGridCellComponent implements OnInit, OnChanges, OnDestroy, CellT
548551
*/
549552
@HostBinding('class.igx-grid__td--valid')
550553
public get isValidAfterEdit() {
551-
const formControl = this.formGroup?.get(this.column?.field);
552-
return this.editMode && formControl && !formControl.invalid && formControl.dirty;
554+
if (this.formGroup) {
555+
const isValidAfterEdit = this.grid.validation?.isFieldValidAfterEdit(this.formGroup, this.column?.field);
556+
return this.editMode && isValidAfterEdit;
557+
}
558+
return false;
553559
}
554560

555561
/**

projects/igniteui-angular/src/lib/grids/grid/grid-cell-selection.spec.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
SelectionWithScrollsComponent,
66
SelectionWithTransactionsComponent,
77
CellSelectionNoneComponent,
8-
CellSelectionSingleComponent
8+
CellSelectionSingleComponent,
9+
IgxGridRowEditingWithoutEditableColumnsComponent
910
} from '../../test-utils/grid-samples.spec';
1011
import { IgxStringFilteringOperand } from '../../data-operations/filtering-condition';
1112
import { UIInteractions, wait } from '../../test-utils/ui-interactions.spec';
@@ -26,7 +27,8 @@ describe('IgxGrid - Cell selection #grid', () => {
2627
SelectionWithScrollsComponent,
2728
SelectionWithTransactionsComponent,
2829
CellSelectionNoneComponent,
29-
CellSelectionSingleComponent
30+
CellSelectionSingleComponent,
31+
IgxGridRowEditingWithoutEditableColumnsComponent
3032
]
3133
}).compileComponents();
3234
}));
@@ -256,6 +258,31 @@ describe('IgxGrid - Cell selection #grid', () => {
256258
expect(grid.selectedCells.length).toBe(1);
257259
});
258260

261+
it('Should not trigger range selection when CellTemplate is used and the user clicks on element inside it', () => {
262+
fix = TestBed.createComponent(IgxGridRowEditingWithoutEditableColumnsComponent);
263+
fix.detectChanges();
264+
265+
const component = fix.componentInstance;
266+
grid = fix.componentInstance.grid;
267+
268+
expect(component.customCell).toBeDefined();
269+
270+
const column = grid.getColumnByName('ProductID');
271+
column.bodyTemplate = component.customCell;
272+
fix.detectChanges();
273+
274+
const selectionChangeSpy = spyOn<any>(grid.rangeSelected, 'emit').and.callThrough();
275+
const cell = grid.gridAPI.get_cell_by_index(1, 'ProductID');
276+
const cellElement = cell.nativeElement;
277+
const span = cellElement.querySelector('span');
278+
279+
expect(span).not.toBeNull();
280+
281+
UIInteractions.simulateClickAndSelectEvent(span);
282+
fix.detectChanges();
283+
expect(selectionChangeSpy).not.toHaveBeenCalled();
284+
});
285+
259286
it('Should be able to select range when click on a cell and hold Shift key and click on another Cell', () => {
260287
const firstCell = grid.gridAPI.get_cell_by_index(3, 'HireDate');
261288
const secondCell = grid.gridAPI.get_cell_by_index(1, 'ID');

projects/igniteui-angular/src/lib/grids/grid/grid-validation.service.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,21 @@ export class IgxGridValidationService {
5959
*/
6060
private addFormControl(formGroup: FormGroup, data: any, column: ColumnType) {
6161
const value = resolveNestedPath(data || {}, columnFieldPath(column.field));
62-
const field = this.getFieldKey(column.field);
6362
const control = new FormControl(value, { updateOn: this.grid.validationTrigger });
6463
control.addValidators(column.validators);
65-
formGroup.addControl(field, control);
64+
formGroup.addControl(column.field, control);
6665
control.setValue(value);
6766
}
68-
67+
6968
/**
7069
* @hidden
7170
* @internal
71+
Wraps the provided path into an array. This way FormGroup.get will return proper result.
72+
Otherwise, if the path is a string (e.g. 'address.street'), FormGroup.get will treat it as there is a nested structure
73+
and will look for control with a name of 'address' which returns undefined.
7274
*/
73-
private getFieldKey(path: string) {
74-
const parts = path?.split('.') ?? [];
75-
return parts.join('_');
75+
private getFormControlPath(path: string): (string)[] {
76+
return [path];
7677
}
7778

7879
/**
@@ -89,8 +90,8 @@ export class IgxGridValidationService {
8990
*/
9091
public getFormControl(rowId: any, columnKey: string) {
9192
const formControl = this.getFormGroup(rowId);
92-
const field = this.getFieldKey(columnKey);
93-
return formControl?.get(field);
93+
const path = this.getFormControlPath(columnKey);
94+
return formControl?.get(path);
9495
}
9596

9697
/**
@@ -101,6 +102,22 @@ export class IgxGridValidationService {
101102
this._validityStates.set(rowId, form);
102103
}
103104

105+
/**
106+
* Checks the validity of the native ngControl
107+
*/
108+
public isFieldInvalid(formGroup: FormGroup, fieldName: string): boolean {
109+
const path = this.getFormControlPath(fieldName);
110+
return formGroup.get(path)?.invalid && formGroup.get(path)?.touched;
111+
}
112+
113+
/**
114+
* Checks the validity of the native ngControl after edit
115+
*/
116+
public isFieldValidAfterEdit(formGroup: FormGroup, fieldName: string): boolean {
117+
const path = this.getFormControlPath(fieldName);
118+
return !formGroup.get(path)?.invalid && formGroup.get(path)?.dirty;
119+
}
120+
104121
/**
105122
* @hidden
106123
* @internal
@@ -110,10 +127,10 @@ export class IgxGridValidationService {
110127
this._validityStates.forEach((formGroup, key) => {
111128
const state: IFieldValidationState[] = [];
112129
for (const col of this.grid.columns) {
113-
const colKey = this.getFieldKey(col.field);
114-
const control = formGroup.get(colKey);
130+
const path = this.getFormControlPath(col.field);
131+
const control = formGroup.get(path);
115132
if (control) {
116-
state.push({ field: colKey, status: control.status as ValidationStatus, errors: control.errors })
133+
state.push({ field: col.field, status: control.status as ValidationStatus, errors: control.errors })
117134
}
118135
}
119136
states.push({ key: key, status: formGroup.status as ValidationStatus, fields: state, errors: formGroup.errors });
@@ -138,8 +155,8 @@ export class IgxGridValidationService {
138155
const keys = Object.keys(rowData);
139156
const rowGroup = this.getFormGroup(rowId);
140157
for (const key of keys) {
141-
const colKey = this.getFieldKey(key);
142-
const control = rowGroup?.get(colKey);
158+
const path = this.getFormControlPath(key);
159+
const control = rowGroup?.get(path);
143160
if (control && control.value !== rowData[key]) {
144161
control.setValue(rowData[key], { emitEvent: false });
145162
}
@@ -174,8 +191,8 @@ export class IgxGridValidationService {
174191
rowGroup.markAsTouched();
175192
const fields = field ? [field] : this.grid.columns.map(x => x.field);
176193
for (const currField of fields) {
177-
const colKey = this.getFieldKey(currField);
178-
rowGroup?.get(colKey)?.markAsTouched();
194+
const path = this.getFormControlPath(currField);
195+
rowGroup?.get(path)?.markAsTouched();
179196
}
180197
}
181198

projects/igniteui-angular/src/lib/grids/grid/grid-validation.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,25 @@ describe('IgxGrid - Validation #grid', () => {
171171
expect(erorrMessage).toEqual(' Entry should be at least 4 character(s) long ');
172172
});
173173

174+
it('should mark invalid cell with igx-grid__td--invalid class and show the related error cell template when the field contains "."', () => {
175+
const grid = fixture.componentInstance.grid as IgxGridComponent;
176+
// add new column
177+
fixture.componentInstance.columns.push({ field: 'New.Column', dataType: 'string' });
178+
fixture.detectChanges();
179+
expect(grid.columns.length).toBe(5);
180+
181+
let cell = grid.gridAPI.get_cell_by_visible_index(1, 4);
182+
UIInteractions.simulateDoubleClickAndSelectEvent(cell.element);
183+
cell.update('asd');
184+
fixture.detectChanges();
185+
186+
cell = grid.gridAPI.get_cell_by_visible_index(1, 4);
187+
//min length should be 4
188+
GridFunctions.verifyCellValid(cell, false);
189+
const erorrMessage = cell.errorTooltip.first.elementRef.nativeElement.children[0].textContent;
190+
expect(erorrMessage).toEqual(' Entry should be at least 4 character(s) long ');
191+
});
192+
174193
it('should show the error message on error icon hover and when the invalid cell becomes active.', fakeAsync(() => {
175194
const grid = fixture.componentInstance.grid as IgxGridComponent;
176195

projects/igniteui-angular/src/lib/grids/hierarchical-grid/hierarchical-grid.selection.spec.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
IgxHierarchicalGridRowSelectionComponent,
1010
IgxHierarchicalGridRowSelectionTestSelectRowOnClickComponent,
1111
IgxHierarchicalGridCustomSelectorsComponent,
12-
IgxHierarchicalGridRowSelectionNoTransactionsComponent
12+
IgxHierarchicalGridRowSelectionNoTransactionsComponent,
13+
IgxHierGridExternalAdvancedFilteringComponent
1314
} from '../../test-utils/hierarchical-grid-components.spec';
1415
import { GridSelectionFunctions, GridFunctions } from '../../test-utils/grid-functions.spec';
1516
import { GridSelectionMode, Size } from '../common/enums';
@@ -33,7 +34,8 @@ describe('IgxHierarchicalGrid selection #hGrid', () => {
3334
IgxHierarchicalGridRowSelectionComponent,
3435
IgxHierarchicalGridRowSelectionTestSelectRowOnClickComponent,
3536
IgxHierarchicalGridCustomSelectorsComponent,
36-
IgxHierarchicalGridRowSelectionNoTransactionsComponent
37+
IgxHierarchicalGridRowSelectionNoTransactionsComponent,
38+
IgxHierGridExternalAdvancedFilteringComponent
3739
]
3840
}).compileComponents();
3941
}))
@@ -474,6 +476,30 @@ describe('IgxHierarchicalGrid selection #hGrid', () => {
474476
GridSelectionFunctions.verifySelectedRange(hierarchicalGrid, 0, 0, 2, 2);
475477
}));
476478

479+
it('Should not trigger range selection when CellTemplate is used and the user clicks on element inside it', () => {
480+
fix = TestBed.createComponent(IgxHierGridExternalAdvancedFilteringComponent);
481+
fix.detectChanges();
482+
483+
const component = fix.componentInstance;
484+
hierarchicalGrid = fix.componentInstance.hgrid;
485+
486+
expect(component.customCell).toBeDefined();
487+
488+
const column = hierarchicalGrid.getColumnByName('ID');
489+
column.bodyTemplate = component.customCell;
490+
fix.detectChanges();
491+
492+
const selectionChangeSpy = spyOn<any>(hierarchicalGrid.rangeSelected, 'emit').and.callThrough();
493+
const cell = hierarchicalGrid.gridAPI.get_cell_by_index(0, 'ID');
494+
const cellElement = cell.nativeElement;
495+
const span = cellElement.querySelector('span');
496+
497+
expect(span).not.toBeNull();
498+
499+
UIInteractions.simulateClickAndSelectEvent(span);
500+
fix.detectChanges();
501+
expect(selectionChangeSpy).not.toHaveBeenCalled();
502+
});
477503
});
478504

479505
describe('Row Selection', () => {

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ export class IgxGridSelectionService {
637637
if (this.areEqualCollections(currSelection, newSelection)) {
638638
return;
639639
}
640-
640+
641641
const args: IRowSelectionEventArgs = {
642642
owner: this.grid,
643643
oldSelection: currSelection,
@@ -857,8 +857,10 @@ export class IgxGridSelectionService {
857857
this.pointerEventInGridBody = false;
858858
this.grid.document.body.removeEventListener('pointerup', this.pointerOriginHandler);
859859

860-
const targetTagName = event.target.tagName.toLowerCase();
861-
if (targetTagName !== 'igx-grid-cell' && targetTagName !== 'igx-tree-grid-cell') {
860+
const gridCellSelectors = ['igx-grid-cell', 'igx-hierarchical-grid-cell', 'igx-tree-grid-cell'];
861+
const isInsideGridCell = gridCellSelectors.some(selector => event.target.closest(selector));
862+
863+
if (!isInsideGridCell) {
862864
this.pointerUp(this._lastSelectedNode, this.grid.rangeSelected, true);
863865
}
864866
};

projects/igniteui-angular/src/lib/grids/tree-grid/tree-grid-selection.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,27 @@ describe('IgxTreeGrid - Selection #tGrid', () => {
964964
expect(treeGrid.selectedCells[0] instanceof IgxGridCell).toBe(true);
965965
expect(treeGrid.selectedCells[0].value).toBe(19);
966966
});
967+
968+
it('Should not trigger range selection when CellTemplate is used and the user clicks on element inside it', () => {
969+
const component = fix.componentInstance;
970+
971+
expect(component.customCell).toBeDefined();
972+
973+
const column = treeGrid.getColumnByName('ID');
974+
column.bodyTemplate = component.customCell;
975+
fix.detectChanges();
976+
977+
const selectionChangeSpy = spyOn<any>(treeGrid.rangeSelected, 'emit').and.callThrough();
978+
const cell = treeGrid.gridAPI.get_cell_by_index(0, 'ID');
979+
const cellElement = cell.nativeElement;
980+
const span = cellElement.querySelector('span');
981+
982+
expect(span).not.toBeNull();
983+
984+
UIInteractions.simulateClickAndSelectEvent(span);
985+
fix.detectChanges();
986+
expect(selectionChangeSpy).not.toHaveBeenCalled();
987+
});
967988
});
968989

969990
describe('Cell/Row Selection With Row Editing', () => {

projects/igniteui-angular/src/lib/test-utils/grid-samples.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,11 +1407,18 @@ export class IgxGridRowEditingComponent extends BasicGridComponent {
14071407
<igx-column field="ReorderLevel" header="Reorder Lever" [dataType]="'number'" [editable]="true" width="100px"></igx-column>
14081408
<igx-column field="ProductName" header="Product Name" [dataType]="'string'" width="150px"></igx-column>
14091409
<igx-column field="OrderDate" header="Order Date" [dataType]="'date'" width="150px" [editable]="false"></igx-column>
1410+
<ng-template #customCell igxCell let-cell="cell" let-val>
1411+
<span style="background-color: red;">val</span>
1412+
<br>
1413+
{{val}}
1414+
</ng-template>
14101415
</igx-grid>`,
14111416
imports: [IgxGridComponent, IgxColumnComponent, IgxCellTemplateDirective]
14121417
})
14131418
export class IgxGridRowEditingWithoutEditableColumnsComponent extends BasicGridComponent {
14141419
public override data = SampleTestData.foodProductData();
1420+
@ViewChild('customCell', { static: true })
1421+
public customCell!: TemplateRef<any>;
14151422
}
14161423

14171424
@Component({

0 commit comments

Comments
 (0)