Skip to content

Commit 213c955

Browse files
authored
Sortable datatable columns (#26)
1 parent e3d31eb commit 213c955

File tree

11 files changed

+142
-95
lines changed

11 files changed

+142
-95
lines changed

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ A collection of reusable components designed for use in Frank!Framework projects
55
![frank-framework-github-banner](banner.png)
66

77
## Available Components
8-
| Component | Selector | Description
9-
| --- | --- | ---
10-
| [Alert](/projects/angular-components/src/lib/alert/) | <ff-alert> | Alert the user, useful for forms, documentation or to give a warning for anything.
11-
| [Button](/projects/angular-components/src/lib/button/) | <ff-button> | Buttons that fit the FF style & can have a toggleable active state
12-
| [Chip](/projects/angular-components/src/lib/chip/) | <ff-chip> | A stylized border around a word or short text, most likely used for labeling
13-
| [Search](/projects/angular-components/src/lib/search/) | <ff-search> | A search field that works like any other form input but doesn't need to be in a form
14-
| [Checkbox](/projects/angular-components/src/lib/checkbox/) | <ff-checkbox> | A custom checkbox using the ff colourscheme
8+
| Component | Selector | Description |
9+
|--------------------------------------------------------------|----------------------|--------------------------------------------------------------------------------------|
10+
| [Alert](/projects/angular-components/src/lib/alert/) | <ff-alert> | Alert the user, useful for forms, documentation or to give a warning for anything |
11+
| [Button](/projects/angular-components/src/lib/button/) | <ff-button> | Buttons that fit the FF style & can have a toggleable active state |
12+
| [Chip](/projects/angular-components/src/lib/chip/) | <ff-chip> | A stylized border around a word or short text, most likely used for labeling |
13+
| [Search](/projects/angular-components/src/lib/search/) | <ff-search> | A search field that works like any other form input but doesn't need to be in a form |
14+
| [Checkbox](/projects/angular-components/src/lib/checkbox/) | <ff-checkbox> | A custom checkbox using the ff colourscheme |
15+
| [Datatable](/projects/angular-components/src/lib/datatable/) | <ff-datatable> | Datatable that is able to handle ng templates & server side data |
1516

1617
## How to use
1718
Install the package from NPM (coming soon)

projects/angular-components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@frankframework/angular-components",
3-
"version": "1.1.7",
3+
"version": "1.2.0",
44
"description": "A collection of reusable components designed for use in Frank!Framework projects",
55
"main": "",
66
"author": "Vivy Booman",

projects/angular-components/src/_index.scss

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33

44
@font-face {
55
font-family: 'Inter';
6-
font-stretch: normal;
76
font-style: normal;
8-
font-weight: 100, 200, 300, 400, 500, 600, 700, 800, 900;
7+
font-weight: 100 900;
8+
font-display: swap;
99
src: url('./assets/Inter-VariableFont_opsz,wght.ttf') format('truetype');
1010
}
1111

1212
@font-face {
1313
font-family: 'Inter';
14-
font-stretch: normal;
1514
font-style: italic;
16-
font-weight: 100, 200, 300, 400, 500, 600, 700, 800, 900;
15+
font-weight: 100 900;
16+
font-display: swap;
1717
src: url('./assets/Inter-Italic-VariableFont_opsz,wght.ttf') format('truetype');
1818
}
1919

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,23 @@
2121
<table cdk-table [dataSource]="datasource" class="table table-striped table-hover">
2222
<ng-container *ngFor="let column of displayColumns">
2323
<ng-container [cdkColumnDef]="column.name">
24-
<th cdk-header-cell *cdkHeaderCellDef [className]="column.className" [hidden]="column.hidden">
25-
{{ column.displayName }}
26-
</th>
24+
@if (this.datasource.options.columnSort && !datasource.options.serverSide && column.sortable) {
25+
<th
26+
cdk-header-cell
27+
*cdkHeaderCellDef
28+
sortable
29+
[columnName]="column.name"
30+
(sorted)="onColumnSort($event)"
31+
[className]="column.className"
32+
[hidden]="column.hidden"
33+
>
34+
{{ column.displayName }}
35+
</th>
36+
} @else {
37+
<th cdk-header-cell *cdkHeaderCellDef [className]="column.className" [hidden]="column.hidden">
38+
{{ column.displayName }}
39+
</th>
40+
}
2741
<td cdk-cell *cdkCellDef="let element" [hidden]="column.hidden">
2842
<ng-container *ngIf="!column.html; else htmlBody"
2943
><ng-container>{{ element[column.property] }}</ng-container>

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1-
import { AfterViewInit, Component, ContentChildren, Input, OnDestroy, QueryList, TemplateRef } from '@angular/core';
1+
import {
2+
AfterViewInit,
3+
Component,
4+
ContentChildren,
5+
Input,
6+
OnDestroy,
7+
QueryList,
8+
TemplateRef,
9+
ViewChildren,
10+
} from '@angular/core';
211
import { CdkTableModule, DataSource } from '@angular/cdk/table';
312
import { CommonModule } from '@angular/common';
413
import { FormsModule } from '@angular/forms';
514
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
615
import { DtContentDirective, DtContent } from './dt-content.directive';
16+
import { basicAnyValueTableSort, SortDirection, SortEvent, ThSortableDirective } from '../th-sortable.directive';
717

818
export type TableOptions = {
919
sizeOptions: number[];
1020
size: number;
11-
serverSide: boolean;
1221
filter: boolean;
22+
serverSide: boolean;
23+
serverSort: SortDirection;
24+
columnSort: boolean;
1325
};
1426

1527
export type DataTableColumn<T> = {
@@ -20,6 +32,7 @@ export type DataTableColumn<T> = {
2032
html?: boolean;
2133
className?: string;
2234
hidden?: boolean;
35+
sortable?: boolean;
2336
};
2437

2538
export type DataTableEntryInfo = {
@@ -37,7 +50,7 @@ export type DataTablePaginationInfo = {
3750
export type DataTableServerRequestInfo = {
3851
size: number;
3952
offset: number;
40-
sort: 'asc' | 'desc';
53+
sort: SortDirection;
4154
};
4255

4356
export type DataTableServerResponseInfo<T> = {
@@ -56,14 +69,15 @@ type ContentTemplate<T> = {
5669
@Component({
5770
selector: 'ff-datatable',
5871
standalone: true,
59-
imports: [CommonModule, FormsModule, CdkTableModule],
72+
imports: [CommonModule, FormsModule, CdkTableModule, ThSortableDirective],
6073
templateUrl: './datatable.component.html',
6174
styleUrl: './datatable.component.scss',
6275
})
6376
export class DatatableComponent<T> implements AfterViewInit, OnDestroy {
6477
@Input({ required: true }) public datasource!: DataTableDataSource<T>;
6578
@Input({ required: true }) public displayColumns: DataTableColumn<T>[] = [];
6679

80+
@ViewChildren(ThSortableDirective) sortableHeaders!: QueryList<ThSortableDirective>;
6781
@ContentChildren(DtContentDirective) protected content!: QueryList<DtContentDirective<T>>;
6882
protected contentTemplates: ContentTemplate<T>[] = [];
6983
protected totalFilteredEntries: number = 0;
@@ -78,6 +92,7 @@ export class DatatableComponent<T> implements AfterViewInit, OnDestroy {
7892
}
7993

8094
private datasourceSubscription: Subscription = new Subscription();
95+
private originalData: T[] | null = null;
8196

8297
ngAfterViewInit(): void {
8398
// needed to avoid ExpressionChangedAfterItHasBeenCheckedError
@@ -105,22 +120,27 @@ export class DatatableComponent<T> implements AfterViewInit, OnDestroy {
105120
this.datasourceSubscription.unsubscribe();
106121
}
107122

108-
applyFilter(event: Event): void {
123+
protected applyFilter(event: Event): void {
109124
const filterValue = (event.target as HTMLInputElement).value;
110125
this.datasource.filter = filterValue.trim();
111126
}
112127

113-
applyPaginationSize(sizeValue: string): void {
128+
protected applyPaginationSize(sizeValue: string): void {
114129
this.datasource.options = { size: +sizeValue };
115130
}
116131

117-
updatePage(pageNumber: number): void {
132+
protected updatePage(pageNumber: number): void {
118133
this.datasource.updatePage(pageNumber);
119134
}
120135

121136
protected findHtmlTemplate(templateName: string): ContentTemplate<T> | undefined {
122137
return this.contentTemplates.find(({ name }) => name === templateName);
123138
}
139+
140+
protected onColumnSort(event: SortEvent): void {
141+
if (this.originalData === null) this.originalData = this.datasource.data;
142+
this.datasource.data = basicAnyValueTableSort(this.originalData, this.sortableHeaders, event);
143+
}
124144
}
125145

126146
export class DataTableDataSource<T> extends DataSource<T> {
@@ -132,6 +152,8 @@ export class DataTableDataSource<T> extends DataSource<T> {
132152
size: 50,
133153
filter: true,
134154
serverSide: false,
155+
serverSort: 'NONE',
156+
columnSort: true,
135157
});
136158
private _entriesInfo = new BehaviorSubject<DataTableEntryInfo>({
137159
minPageEntry: 0,
@@ -147,7 +169,6 @@ export class DataTableDataSource<T> extends DataSource<T> {
147169
private _entriesInfo$ = this._entriesInfo.asObservable();
148170

149171
private filteredData: T[] = [];
150-
private serverRequestId: number = -1;
151172
private serverRequestFn?: (value: DataTableServerRequestInfo) => PromiseLike<DataTableServerResponseInfo<T>>;
152173

153174
get data(): T[] {
@@ -237,7 +258,7 @@ export class DataTableDataSource<T> extends DataSource<T> {
237258
Promise.resolve<DataTableServerRequestInfo>({
238259
size: this.options.size,
239260
offset: (this.currentPage - 1) * this.options.size,
240-
sort: 'asc',
261+
sort: this.options.serverSort,
241262
})
242263
.then(this.serverRequestFn)
243264
.then((response) => {

projects/angular-components/src/lib/th-sortable.directive.spec.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,28 @@ import { Component, DebugElement, QueryList, ViewChildren } from '@angular/core'
22
import { SortEvent, ThSortableDirective, basicTableSort } from './th-sortable.directive';
33
import { ComponentFixture, TestBed } from '@angular/core/testing';
44
import { By } from '@angular/platform-browser';
5-
import { NgForOf } from '@angular/common';
65

76
@Component({
87
standalone: true,
98
template: `
109
<table>
1110
<thead>
1211
<tr>
13-
<th sortable="name" (sorted)="onSort($event)">Name</th>
14-
<th sortable="value" (sorted)="onSort($event)">Size</th>
12+
<th sortColumnName="name" (sorted)="onSort($event)">Name</th>
13+
<th sortColumnName="value" (sorted)="onSort($event)">Size</th>
1514
</tr>
1615
</thead>
1716
<tbody>
18-
<tr *ngFor="let item of items">
19-
<td>{{ item.name }}</td>
20-
<td>{{ item.value }}</td>
21-
</tr>
17+
@for (item of items; track item.value) {
18+
<tr>
19+
<td>{{ item.name }}</td>
20+
<td>{{ item.value }}</td>
21+
</tr>
22+
}
2223
</tbody>
2324
</table>
2425
`,
25-
imports: [ThSortableDirective, NgForOf],
26+
imports: [ThSortableDirective],
2627
})
2728
class TestComponent {
2829
items = [
@@ -50,13 +51,13 @@ describe('ThSortableDirective', () => {
5051
directiveElements = fixture.debugElement.queryAll(By.directive(ThSortableDirective));
5152
});
5253

53-
it('on click switches from asc to desc', () => {
54+
it('on click switches from ASC to DESC', () => {
5455
directiveElements[0].nativeElement.click();
5556
const directiveInstance = directiveElements[0].injector.get(ThSortableDirective);
5657

57-
expect(directiveInstance.direction).toBe('asc');
58+
expect(directiveInstance.direction).toBe('ASC');
5859
directiveElements[0].nativeElement.click();
59-
expect(directiveInstance.direction).toBe('desc');
60+
expect(directiveInstance.direction).toBe('DESC');
6061
});
6162

6263
it('sorts table rows', () => {
@@ -66,28 +67,28 @@ describe('ThSortableDirective', () => {
6667
const directive1Element = directiveElements[1].nativeElement;
6768

6869
directive0Element.click();
69-
expect(directive0Instance.direction).toBe('asc');
70+
expect(directive0Instance.direction).toBe('ASC');
7071
expect(fixture.componentInstance.items[0]).toEqual({
7172
name: 'a',
7273
value: 2,
7374
});
7475

7576
directive0Element.click();
76-
expect(directive0Instance.direction).toBe('desc');
77+
expect(directive0Instance.direction).toBe('DESC');
7778
expect(fixture.componentInstance.items[0]).toEqual({
7879
name: 'b',
7980
value: 1,
8081
});
8182

8283
directive1Element.click();
83-
expect(directive1Instance.direction).toBe('asc');
84+
expect(directive1Instance.direction).toBe('ASC');
8485
expect(fixture.componentInstance.items[0]).toEqual({
8586
name: 'b',
8687
value: 1,
8788
});
8889

8990
directive1Element.click();
90-
expect(directive1Instance.direction).toBe('desc');
91+
expect(directive1Instance.direction).toBe('DESC');
9192
expect(fixture.componentInstance.items[0]).toEqual({
9293
name: 'a',
9394
value: 2,

0 commit comments

Comments
 (0)