Skip to content

Commit b8f7ea1

Browse files
feat: support bulk actions via summary row directive
Add `DatatableSummaryRowDirective` (`[ngx-datatable-summary-row]`) that allows rendering a bulk actions bar in the summary row position. When rows are selected, users can project action controls (e.g. delete, export) that appear as a sticky row at the top of the datatable body. The directive reuses the existing summary row infrastructure, extending it with a template input that bypasses per-column computation and renders the provided content directly. Usage: ```html <ngx-datatable [rows]="rows" selectionType="checkbox" [(selected)]="selected"> @if (selected.length) { <ng-template ngx-datatable-summary-row> <span>{{ selected.length }} selected</span> <button (click)="delete()">Delete</button> </ng-template> } </ngx-datatable> ```
1 parent 05f978c commit b8f7ea1

14 files changed

Lines changed: 264 additions & 18 deletions

playwright/e2e/selection.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,34 @@ test.describe('selection', () => {
302302
});
303303
});
304304
});
305+
306+
test.describe('bulk actions', () => {
307+
const example = 'bulk-actions-selection';
308+
309+
test('bulk actions row is hidden when no rows are selected', async ({ si, page }) => {
310+
await si.visitExample(example);
311+
312+
const summaryRow = page.locator('datatable-summary-row.sticky');
313+
await expect(summaryRow).toHaveCount(0);
314+
});
315+
316+
test('bulk actions row appears on checkbox selection', async ({ si, page }) => {
317+
await si.visitExample(example);
318+
319+
const checkbox = page.locator('datatable-body-row input[type=checkbox]').first();
320+
await checkbox.check();
321+
322+
const summaryRow = page.locator('datatable-summary-row.sticky');
323+
await expect(summaryRow).toBeVisible();
324+
await expect(page.locator('.bulk-actions-count')).toContainText('1 row(s) selected');
325+
326+
await si.runVisualAndA11yTests({
327+
step: 'bulk-actions-single-selection',
328+
axeRulesSet: [
329+
{ id: 'label', enabled: false },
330+
{ id: 'empty-table-header', enabled: false }
331+
]
332+
});
333+
});
334+
});
305335
});
Lines changed: 3 additions & 0 deletions
Loading

projects/ngx-datatable/src/lib/components/body/body.component.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,14 @@ import { DataTableSummaryRowComponent } from './summary/summary-row.component';
9999
[class.horizontal-overflow]="innerWidth() < (columnGroupWidths?.total ?? 0)"
100100
(scroll)="onBodyScroll($event)"
101101
>
102-
@if (summaryRow() && summaryPosition() === 'top') {
102+
@if ((summaryRow() || summaryRowTemplate()) && summaryPosition() === 'top') {
103103
<datatable-summary-row
104+
[class.sticky]="summaryRowTemplate()"
104105
[rowHeight]="summaryHeight()"
105106
[innerWidth]="innerWidth()"
106107
[rows]="rows"
107108
[columns]="columns"
109+
[template]="summaryRowTemplate()"
108110
/>
109111
}
110112
<ng-template
@@ -233,12 +235,13 @@ import { DataTableSummaryRowComponent } from './summary/summary-row.component';
233235
}
234236
</div>
235237
</datatable-scroller>
236-
@if (summaryRow() && summaryPosition() === 'bottom') {
238+
@if ((summaryRow() || summaryRowTemplate()) && summaryPosition() === 'bottom') {
237239
<datatable-summary-row
238240
[rowHeight]="summaryHeight()"
239241
[innerWidth]="innerWidth()"
240242
[rows]="rows"
241243
[columns]="columns"
244+
[template]="summaryRowTemplate()"
242245
/>
243246
}
244247
}
@@ -291,6 +294,7 @@ export class DataTableBodyComponent<TRow extends Row = any> implements OnInit, O
291294
readonly summaryRow = input<boolean>();
292295
readonly summaryPosition = input.required<string>();
293296
readonly summaryHeight = input.required<number>();
297+
readonly summaryRowTemplate = input<TemplateRef<void>>();
294298
readonly rowDraggable = input<boolean>();
295299
readonly rowDragEvents = input.required<OutputEmitterRef<DragEventData>>();
296300
readonly disableRowCheck = input<(row: TRow) => boolean | undefined>();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:host(.sticky) {
2+
position: sticky;
3+
top: 0;
4+
z-index: 2;
5+
}

projects/ngx-datatable/src/lib/components/body/summary/summary-row.component.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
2-
import { ComponentRef } from '@angular/core';
2+
import { Component, ComponentRef, TemplateRef, viewChild } from '@angular/core';
33
import { ComponentFixture, TestBed } from '@angular/core/testing';
44

55
import { TableColumnInternal } from '../../../types/internal.types';
@@ -155,3 +155,39 @@ describe('DataTableSummaryRowComponent', () => {
155155
});
156156
});
157157
});
158+
159+
@Component({
160+
imports: [DataTableSummaryRowComponent],
161+
template: `
162+
<datatable-summary-row
163+
[rows]="rows"
164+
[columns]="columns"
165+
[rowHeight]="30"
166+
[innerWidth]="100"
167+
[template]="tpl()"
168+
/>
169+
<ng-template #summaryTpl>
170+
<span class="custom-content">Custom summary content</span>
171+
</ng-template>
172+
`
173+
})
174+
class TestHostComponent {
175+
rows = [{ col1: 10 }];
176+
columns: TableColumnInternal[] = toInternalColumn([{ prop: 'col1' }]);
177+
readonly tpl = viewChild<TemplateRef<void>>('summaryTpl');
178+
}
179+
180+
describe('DataTableSummaryRowComponent with template', () => {
181+
let fixture: ComponentFixture<TestHostComponent>;
182+
183+
beforeEach(async () => {
184+
fixture = TestBed.createComponent(TestHostComponent);
185+
fixture.detectChanges();
186+
});
187+
188+
it('should render custom template content instead of computed columns', () => {
189+
const el: HTMLElement = fixture.nativeElement;
190+
expect(el.querySelector('.custom-content')?.textContent).toBe('Custom summary content');
191+
expect(el.querySelector('datatable-body-row')).toBeNull();
192+
});
193+
});

projects/ngx-datatable/src/lib/components/body/summary/summary-row.component.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Component, computed, input } from '@angular/core';
1+
import { NgTemplateOutlet } from '@angular/common';
2+
import { Component, computed, input, TemplateRef } from '@angular/core';
23

34
import { TableColumnInternal } from '../../../types/internal.types';
45
import { DataTableBodyRowComponent } from '../body-row.component';
@@ -22,21 +23,36 @@ const noopSumFunc = (cells: any[]): void => {
2223

2324
@Component({
2425
selector: 'datatable-summary-row',
25-
imports: [DataTableBodyRowComponent],
26+
imports: [DataTableBodyRowComponent, NgTemplateOutlet],
2627
template: `
27-
@let summaryRow = this.summaryRow();
28-
@let _internalColumns = this._internalColumns();
29-
@if (summaryRow && _internalColumns.length) {
30-
<datatable-body-row
31-
ariaRowCheckboxMessage=""
32-
[columns]="_internalColumns"
33-
[rowHeight]="rowHeight()"
34-
[row]="summaryRow"
35-
[rowIndex]="{ index: -1 }"
36-
[cssClasses]="{}"
37-
/>
28+
@let template = this.template();
29+
@if (template) {
30+
<div
31+
class="datatable-body-row"
32+
role="row"
33+
[style.height.px]="rowHeight()"
34+
[style.width.px]="innerWidth()"
35+
>
36+
<div class="datatable-body-cell" role="cell" [style.width.px]="innerWidth()">
37+
<ng-container [ngTemplateOutlet]="template" />
38+
</div>
39+
</div>
40+
} @else {
41+
@let summaryRow = this.summaryRow();
42+
@let _internalColumns = this._internalColumns();
43+
@if (summaryRow && _internalColumns.length) {
44+
<datatable-body-row
45+
ariaRowCheckboxMessage=""
46+
[columns]="_internalColumns"
47+
[rowHeight]="rowHeight()"
48+
[row]="summaryRow"
49+
[rowIndex]="{ index: -1 }"
50+
[cssClasses]="{}"
51+
/>
52+
}
3853
}
3954
`,
55+
styleUrl: './summary-row.component.scss',
4056
host: {
4157
class: 'datatable-summary-row'
4258
}
@@ -47,6 +63,7 @@ export class DataTableSummaryRowComponent {
4763

4864
readonly rowHeight = input.required<number>();
4965
readonly innerWidth = input.required<number>();
66+
readonly template = input<TemplateRef<void>>();
5067

5168
protected readonly _internalColumns = computed(() => {
5269
return this.columns().map(col => ({
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Directive, inject, TemplateRef } from '@angular/core';
2+
3+
/**
4+
* Directive that provides custom content for the summary row.
5+
* When applied, the summary row renders the provided template instead of
6+
* computing per-column aggregate values, enabling use cases like bulk action bars.
7+
*
8+
* The row is rendered with sticky positioning at the top of the datatable body.
9+
*
10+
* @example
11+
* ```html
12+
* <ngx-datatable [rows]="rows" selectionType="checkbox" [(selected)]="selected">
13+
* @if (selected.length) {
14+
* <ng-template ngx-datatable-summary-row>
15+
* <span>{{ selected.length }} selected</span>
16+
* <button (click)="delete()">Delete</button>
17+
* </ng-template>
18+
* }
19+
* </ngx-datatable>
20+
* ```
21+
*/
22+
@Directive({
23+
selector: '[ngx-datatable-summary-row]'
24+
})
25+
export class DatatableSummaryRowDirective {
26+
readonly template = inject(TemplateRef<void>);
27+
}

projects/ngx-datatable/src/lib/components/datatable.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
[summaryRow]="summaryRow()"
6060
[summaryHeight]="summaryHeight()"
6161
[summaryPosition]="summaryPosition()"
62+
[summaryRowTemplate]="summaryRowDirective()?.template"
6263
[verticalScrollVisible]="verticalScrollVisible"
6364
[ariaRowCheckboxMessage]="messages().ariaRowCheckboxMessage ?? 'Select row'"
6465
[ariaGroupHeaderCheckboxMessage]="

projects/ngx-datatable/src/lib/components/datatable.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { DatatableGroupHeaderDirective } from './body/body-group-header.directiv
6969
import { DatatableRowDefDirective } from './body/body-row-def.component';
7070
import { DataTableBodyComponent } from './body/body.component';
7171
import { ProgressBarComponent } from './body/progress-bar.component';
72+
import { DatatableSummaryRowDirective } from './body/summary/summary-row.directive';
7273
import { DataTableColumnDirective } from './columns/column.directive';
7374
import { DataTableFooterComponent } from './footer/footer.component';
7475
import { DatatableFooterDirective } from './footer/footer.directive';
@@ -538,6 +539,11 @@ export class DatatableComponent<TRow extends Row = any>
538539
@ContentChild(DatatableGroupHeaderDirective)
539540
groupHeader?: DatatableGroupHeaderDirective;
540541

542+
/**
543+
* Custom summary row template gathered from the ContentChild
544+
*/
545+
readonly summaryRowDirective = contentChild(DatatableSummaryRowDirective);
546+
541547
/**
542548
* Footer template gathered from the ContentChild
543549
* @internal

projects/ngx-datatable/src/lib/ngx-datatable.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DatatableRowDefComponent,
77
DatatableRowDefDirective
88
} from './components/body/body-row-def.component';
9+
import { DatatableSummaryRowDirective } from './components/body/summary/summary-row.directive';
910
import { DataTableColumnCellDirective } from './components/columns/column-cell.directive';
1011
import { DataTableColumnGhostCellDirective } from './components/columns/column-ghost-cell.directive';
1112
import { DataTableColumnHeaderDirective } from './components/columns/column-header.directive';
@@ -37,7 +38,8 @@ import { AllPartial, NgxDatatableConfig, providedNgxDatatableConfig } from './ng
3738
DatatableGroupHeaderTemplateDirective,
3839
DisableRowDirective,
3940
DatatableRowDefComponent,
40-
DatatableRowDefDirective
41+
DatatableRowDefDirective,
42+
DatatableSummaryRowDirective
4143
],
4244
exports: [
4345
DatatableComponent,
@@ -55,7 +57,8 @@ import { AllPartial, NgxDatatableConfig, providedNgxDatatableConfig } from './ng
5557
DatatableGroupHeaderTemplateDirective,
5658
DisableRowDirective,
5759
DatatableRowDefComponent,
58-
DatatableRowDefDirective
60+
DatatableRowDefDirective,
61+
DatatableSummaryRowDirective
5962
]
6063
})
6164
export class NgxDatatableModule {

0 commit comments

Comments
 (0)