Skip to content

Commit a578537

Browse files
authored
Merge pull request UiPath#262 from UiPath/feat/expandable_rows
feat(grid): multiple expanded rows
2 parents d844649 + 14b7bb3 commit a578537

File tree

6 files changed

+195
-16
lines changed

6 files changed

+195
-16
lines changed

projects/angular/components/ui-grid/src/_ui-grid.theme.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ $ui-grid-opacity-transition: opacity $ui-grid-default-transition;
145145
}
146146

147147
&-header-row,
148-
&-row {
148+
&-row,
149+
&-row-state-expanded {
149150
border-bottom-color: $ui-grid-border-color;
150151
transition: $ui-grid-opacity-transition;
151152
}

projects/angular/components/ui-grid/src/ui-grid.component.html

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@
240240
<ng-container *ngTemplateOutlet="rowTemplate; context: {
241241
data: row,
242242
index: index,
243-
expanded: expandedEntry,
243+
expanded: expandedEntries,
244244
last: last
245245
}">
246246
</ng-container>
@@ -255,7 +255,7 @@
255255
<ng-container *ngTemplateOutlet="rowTemplate; context: {
256256
data: row,
257257
index: index,
258-
expanded: expandedEntry,
258+
expanded: expandedEntries,
259259
last: last
260260
}">
261261
</ng-container>
@@ -280,12 +280,14 @@
280280
let-index="index">
281281
<div cdkMonitorSubtreeFocus
282282
class="ui-grid-row"
283-
*ngIf="expanded?.id !== row?.id || expandMode === 'preserve'"
284-
[class.ui-grid-row-state-expanded]="expanded?.id === row?.id"
283+
*ngIf="!isRowExpanded(row?.id) || expandMode === 'preserve'"
284+
[class.ui-grid-row-state-expanded]="isRowExpanded(row?.id)"
285285
[class.ui-grid-border-none]="!footer && last"
286286
[ngClass]="rowConfig?.ngClassFn(row) ?? ''"
287287
[tabIndex]="0"
288288
[attr.data-row-index]="index"
289+
(click)="onRowClick($event, row)"
290+
(keyup.enter)="onRowClick($event, row)"
289291
role="row">
290292

291293
<div *ngIf="isProjected"
@@ -379,8 +381,8 @@
379381
</div>
380382
</div>
381383
</div>
382-
<div *ngIf="expanded?.id === row?.id"
383-
[class.ui-grid-row-state-expanded]="expanded?.id === row?.id"
384+
<div *ngIf="isRowExpanded(row?.id)"
385+
[class.ui-grid-row-state-expanded]="isRowExpanded(row?.id)"
384386
[class.ui-grid-border-none]="!footer && last"
385387
role="row">
386388
<div class="ui-grid-expanded-row-container">

projects/angular/components/ui-grid/src/ui-grid.component.scss

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ ui-grid {
385385
width: 100%;
386386
height: 100%;
387387
padding: $ui-grid-default-spacing;
388+
box-sizing: border-box;
388389
}
389390

390391
.ui-grid-row {
@@ -397,7 +398,8 @@ ui-grid {
397398
}
398399

399400
.ui-grid-header-row,
400-
.ui-grid-row {
401+
.ui-grid-row,
402+
.ui-grid-row-state-expanded {
401403
display: flex;
402404
align-items: center;
403405
border-width: 0;

projects/angular/components/ui-grid/src/ui-grid.component.spec.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,15 @@ describe('Component: UiGrid', () => {
349349
expect(secondCell.nativeElement.getAttribute('role')).toBe('rowheader');
350350
expect(thirdCell.nativeElement.getAttribute('role')).toBe('gridcell');
351351
}));
352+
353+
it('should emit event when row is clicked', () => {
354+
spyOn(grid.rowClick, 'emit');
355+
const firstRow = fixture.debugElement.query(By.css('.ui-grid-row'));
356+
firstRow.nativeElement.dispatchEvent(EventGenerator.click);
357+
expect(grid.rowClick.emit).toHaveBeenCalledWith(jasmine.objectContaining({
358+
row: data[0],
359+
}));
360+
});
352361
});
353362
});
354363

@@ -3954,4 +3963,125 @@ describe('Component: UiGrid', () => {
39543963
expect(colTitleParagraphElement!.getAttribute('aria-label')).toEqual('Number Header');
39553964
}));
39563965
});
3966+
3967+
@Component({
3968+
template: `
3969+
<ui-grid [data]="data"
3970+
[expandedEntry]="expandedEntry"
3971+
[expandedEntries]="expandedEntries">
3972+
<ui-grid-column [property]="'myNumber'"
3973+
title="Number Header"
3974+
width="25%">
3975+
</ui-grid-column>
3976+
3977+
<ui-grid-column [property]="'myBool'"
3978+
title="Boolean Header"
3979+
width="25%">
3980+
</ui-grid-column>
3981+
<ui-grid-expanded-row>
3982+
<ng-template let-entry="data">
3983+
<div class="expanded-row">
3984+
<h2>Expanded row ID: {{ entry.id }}</h2>
3985+
</div>
3986+
</ng-template>
3987+
</ui-grid-expanded-row>
3988+
</ui-grid>
3989+
`,
3990+
})
3991+
class TestFixtureExpandedGridComponent {
3992+
@ViewChild(UiGridComponent, {
3993+
static: true,
3994+
})
3995+
grid!: UiGridComponent<ITestEntity>;
3996+
3997+
data: ITestEntity[] = [];
3998+
expandedEntries?: ITestEntity[];
3999+
expandedEntry?: ITestEntity;
4000+
}
4001+
describe('Scenario: expanded grid rows', () => {
4002+
let fixture: ComponentFixture<TestFixtureExpandedGridComponent>;
4003+
let component: TestFixtureExpandedGridComponent;
4004+
let data: ITestEntity[];
4005+
let grid: UiGridComponent<ITestEntity>;
4006+
4007+
beforeEach(() => {
4008+
TestBed.configureTestingModule({
4009+
imports: [
4010+
UiGridModule,
4011+
NoopAnimationsModule,
4012+
],
4013+
declarations: [TestFixtureExpandedGridComponent],
4014+
});
4015+
4016+
fixture = TestBed.createComponent(TestFixtureExpandedGridComponent);
4017+
component = fixture.componentInstance;
4018+
data = generateListFactory(generateEntity)(6);
4019+
component.data = data;
4020+
grid = component.grid;
4021+
fixture.detectChanges();
4022+
});
4023+
4024+
afterEach(() => {
4025+
fixture.destroy();
4026+
});
4027+
4028+
it('should handle expanded row via expandedEntry', () => {
4029+
component.expandedEntry = data[0];
4030+
fixture.detectChanges();
4031+
4032+
let expandedRows = fixture.debugElement.queryAll(By.css('.expanded-row'));
4033+
expect(expandedRows.length).toBe(1);
4034+
expect((grid.expandedEntries as ITestEntity[])[0].id).toBe(component.expandedEntry.id);
4035+
4036+
component.expandedEntry = data[1];
4037+
fixture.detectChanges();
4038+
4039+
expandedRows = fixture.debugElement.queryAll(By.css('.expanded-row'));
4040+
expect(expandedRows.length).toBe(1);
4041+
expect((grid.expandedEntries as ITestEntity[])[0].id).toBe(component.expandedEntry.id);
4042+
4043+
component.expandedEntry = undefined;
4044+
fixture.detectChanges();
4045+
4046+
expandedRows = fixture.debugElement.queryAll(By.css('.expanded-row'));
4047+
expect(expandedRows).toBeEmptyArray();
4048+
});
4049+
4050+
it('should not have any expanded rows', () => {
4051+
expect(component.expandedEntries).toBeUndefined();
4052+
4053+
const expandedRows = fixture.debugElement.queryAll(By.css('.expanded-row'));
4054+
expect(expandedRows).toBeEmptyArray();
4055+
});
4056+
4057+
it('should have a single expanded row', fakeAsync(() => {
4058+
component.expandedEntries = [data[0]];
4059+
fixture.detectChanges();
4060+
4061+
const expandedRows = fixture.debugElement.queryAll(By.css('.expanded-row'));
4062+
expect(expandedRows.length).toBe(1);
4063+
}));
4064+
4065+
it('should collapse row', fakeAsync(() => {
4066+
component.expandedEntries = [data[0]];
4067+
fixture.detectChanges();
4068+
4069+
let expandedRows = fixture.debugElement.queryAll(By.css('.expanded-row'));
4070+
expect(expandedRows.length).toBe(1);
4071+
4072+
component.expandedEntries = undefined;
4073+
fixture.detectChanges();
4074+
4075+
expandedRows = fixture.debugElement.queryAll(By.css('.expanded-row'));
4076+
expect(expandedRows).toBeEmptyArray();
4077+
}));
4078+
4079+
it('should have multiple expanded rows', () => {
4080+
fixture.componentInstance.expandedEntries = [...data];
4081+
fixture.detectChanges();
4082+
4083+
const expandedRows = fixture.debugElement.queryAll(By.css('.expanded-row'));
4084+
expect(expandedRows.length).toBe(fixture.componentInstance.expandedEntries.length);
4085+
});
4086+
});
39574087
});

projects/angular/components/ui-grid/src/ui-grid.component.ts

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import { ResizableGrid } from './managers/resize/types';
8383
import {
8484
GridOptions,
8585
IFilterModel,
86+
IGridDataEntry,
8687
ISortModel,
8788
} from './models';
8889
import { UiGridIntl } from './ui-grid.intl';
@@ -128,7 +129,7 @@ const FOCUSABLE_ELEMENTS_QUERY = 'a, button:not([hidden]), input:not([hidden]),
128129
changeDetection: ChangeDetectionStrategy.OnPush,
129130
encapsulation: ViewEncapsulation.None,
130131
})
131-
export class UiGridComponent<T extends { id: number | string }> extends ResizableGrid<T> implements AfterContentInit, OnChanges, OnDestroy {
132+
export class UiGridComponent<T extends IGridDataEntry> extends ResizableGrid<T> implements AfterContentInit, OnChanges, OnDestroy {
132133
/**
133134
* The data list that needs to be rendered within the grid.
134135
*
@@ -318,9 +319,31 @@ export class UiGridComponent<T extends { id: number | string }> extends Resizabl
318319
/**
319320
* Set the expanded entry.
320321
*
322+
* @deprecated Use `expandedEntries` instead.
321323
*/
322324
@Input()
323-
expandedEntry?: T;
325+
set expandedEntry(entry: T | undefined) {
326+
this.expandedEntries = entry;
327+
}
328+
get expandedEntry() {
329+
return this._expandedEntries[0];
330+
}
331+
332+
/**
333+
* Set the expanded entry / entries.
334+
*
335+
*/
336+
@Input()
337+
set expandedEntries(entry: T | T[] | undefined) {
338+
if (!entry) {
339+
this._expandedEntries = [];
340+
return;
341+
}
342+
this._expandedEntries = Array.isArray(entry) ? entry : [entry];
343+
}
344+
get expandedEntries() {
345+
return this._expandedEntries;
346+
}
324347

325348
/**
326349
* Configure if the expanded entry should replace the active row, or add a new row with the expanded view.
@@ -381,6 +404,13 @@ export class UiGridComponent<T extends { id: number | string }> extends Resizabl
381404
@Output()
382405
removeCustomFilter = new EventEmitter<void>();
383406

407+
/**
408+
* Emits an event when a row is clicked.
409+
*
410+
*/
411+
@Output()
412+
rowClick = new EventEmitter<{event: Event; row: T}>();
413+
384414
/**
385415
* Emits the column definitions when their definition changes.
386416
*
@@ -632,7 +662,7 @@ export class UiGridComponent<T extends { id: number | string }> extends Resizabl
632662
private _isShiftPressed = false;
633663
private _lastCheckboxIdx = 0;
634664
private _resizeSubscription$: null | Subscription = null;
635-
665+
private _expandedEntries: T[] = [];
636666
/**
637667
* @ignore
638668
*/
@@ -933,6 +963,21 @@ export class UiGridComponent<T extends { id: number | string }> extends Resizabl
933963
this.filterManager.clearCustomFilters();
934964
}
935965

966+
isRowExpanded(rowId?: IGridDataEntry['id']) {
967+
if (rowId == null) {
968+
return false;
969+
}
970+
971+
return this._expandedEntries.some(el => el.id === rowId);
972+
}
973+
974+
onRowClick(event: Event, row: T) {
975+
this.rowClick.emit({
976+
event,
977+
row,
978+
});
979+
}
980+
936981
checkIndeterminateState(indeterminateState: boolean) {
937982
// If the grid has disabled rows the indeterminate can be set to false and still not have all the rows selected,
938983
// in that case we set the indeterminate to true

projects/playground/src/app/pages/grid/component/grid.component.html

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
[virtualScroll]="inputs.virtualScroll"
1313
[showPaintTime]="inputs.showPaintTime"
1414
[showHeaderRow]="inputs.showHeaderRow"
15-
[expandedEntry]="editedEntity"
15+
[expandedEntries]="editedEntity"
1616
[expandMode]="'preserve'"
1717
[customFilterValue]="inputs.customFilter ? [{property: 'parity', method: 'eq', value: 'odd'}] : []">
1818

@@ -74,12 +74,11 @@
7474
</ui-grid-column>
7575

7676
<ui-grid-row-action>
77-
<ng-template let-index="index"
78-
let-entry="data">
79-
<button (click)="editedEntity = editedEntity && index == editedIndex ? undefined : entry; editedIndex = editedIndex === index ? undefined : index;"
77+
<ng-template let-entry="data">
78+
<button (click)="editedEntity = editedEntity?.id === entry.id ? undefined : entry"
8079
mat-icon-button
8180
type="button">
82-
<mat-icon>{{index === editedIndex && editedEntity ? 'close' : 'edit'}}</mat-icon>
81+
<mat-icon>{{editedEntity?.id === entry.id ? 'close' : 'edit'}}</mat-icon>
8382
</button>
8483
</ng-template>
8584
</ui-grid-row-action>

0 commit comments

Comments
 (0)