- |
- {{ column.displayName }}
- |
+ @if (this.datasource.options.columnSort && !datasource.options.serverSide && column.sortable) {
+
+ {{ column.displayName }}
+ |
+ } @else {
+
+ {{ column.displayName }}
+ |
+ }
{{ element[column.property] }}
diff --git a/projects/angular-components/src/lib/datatable/datatable.component.ts b/projects/angular-components/src/lib/datatable/datatable.component.ts
index b7e8aad..e05cf07 100644
--- a/projects/angular-components/src/lib/datatable/datatable.component.ts
+++ b/projects/angular-components/src/lib/datatable/datatable.component.ts
@@ -1,15 +1,27 @@
-import { AfterViewInit, Component, ContentChildren, Input, OnDestroy, QueryList, TemplateRef } from '@angular/core';
+import {
+ AfterViewInit,
+ Component,
+ ContentChildren,
+ Input,
+ OnDestroy,
+ QueryList,
+ TemplateRef,
+ ViewChildren,
+} from '@angular/core';
import { CdkTableModule, DataSource } from '@angular/cdk/table';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { DtContentDirective, DtContent } from './dt-content.directive';
+import { basicAnyValueTableSort, SortDirection, SortEvent, ThSortableDirective } from '../th-sortable.directive';
export type TableOptions = {
sizeOptions: number[];
size: number;
- serverSide: boolean;
filter: boolean;
+ serverSide: boolean;
+ serverSort: SortDirection;
+ columnSort: boolean;
};
export type DataTableColumn = {
@@ -20,6 +32,7 @@ export type DataTableColumn = {
html?: boolean;
className?: string;
hidden?: boolean;
+ sortable?: boolean;
};
export type DataTableEntryInfo = {
@@ -37,7 +50,7 @@ export type DataTablePaginationInfo = {
export type DataTableServerRequestInfo = {
size: number;
offset: number;
- sort: 'asc' | 'desc';
+ sort: SortDirection;
};
export type DataTableServerResponseInfo = {
@@ -56,7 +69,7 @@ type ContentTemplate = {
@Component({
selector: 'ff-datatable',
standalone: true,
- imports: [CommonModule, FormsModule, CdkTableModule],
+ imports: [CommonModule, FormsModule, CdkTableModule, ThSortableDirective],
templateUrl: './datatable.component.html',
styleUrl: './datatable.component.scss',
})
@@ -64,6 +77,7 @@ export class DatatableComponent implements AfterViewInit, OnDestroy {
@Input({ required: true }) public datasource!: DataTableDataSource;
@Input({ required: true }) public displayColumns: DataTableColumn[] = [];
+ @ViewChildren(ThSortableDirective) sortableHeaders!: QueryList;
@ContentChildren(DtContentDirective) protected content!: QueryList>;
protected contentTemplates: ContentTemplate[] = [];
protected totalFilteredEntries: number = 0;
@@ -78,6 +92,7 @@ export class DatatableComponent implements AfterViewInit, OnDestroy {
}
private datasourceSubscription: Subscription = new Subscription();
+ private originalData: T[] | null = null;
ngAfterViewInit(): void {
// needed to avoid ExpressionChangedAfterItHasBeenCheckedError
@@ -105,22 +120,27 @@ export class DatatableComponent implements AfterViewInit, OnDestroy {
this.datasourceSubscription.unsubscribe();
}
- applyFilter(event: Event): void {
+ protected applyFilter(event: Event): void {
const filterValue = (event.target as HTMLInputElement).value;
this.datasource.filter = filterValue.trim();
}
- applyPaginationSize(sizeValue: string): void {
+ protected applyPaginationSize(sizeValue: string): void {
this.datasource.options = { size: +sizeValue };
}
- updatePage(pageNumber: number): void {
+ protected updatePage(pageNumber: number): void {
this.datasource.updatePage(pageNumber);
}
protected findHtmlTemplate(templateName: string): ContentTemplate | undefined {
return this.contentTemplates.find(({ name }) => name === templateName);
}
+
+ protected onColumnSort(event: SortEvent): void {
+ if (this.originalData === null) this.originalData = this.datasource.data;
+ this.datasource.data = basicAnyValueTableSort(this.originalData, this.sortableHeaders, event);
+ }
}
export class DataTableDataSource extends DataSource {
@@ -132,6 +152,8 @@ export class DataTableDataSource extends DataSource {
size: 50,
filter: true,
serverSide: false,
+ serverSort: 'NONE',
+ columnSort: true,
});
private _entriesInfo = new BehaviorSubject({
minPageEntry: 0,
@@ -147,7 +169,6 @@ export class DataTableDataSource extends DataSource {
private _entriesInfo$ = this._entriesInfo.asObservable();
private filteredData: T[] = [];
- private serverRequestId: number = -1;
private serverRequestFn?: (value: DataTableServerRequestInfo) => PromiseLike>;
get data(): T[] {
@@ -237,7 +258,7 @@ export class DataTableDataSource extends DataSource {
Promise.resolve({
size: this.options.size,
offset: (this.currentPage - 1) * this.options.size,
- sort: 'asc',
+ sort: this.options.serverSort,
})
.then(this.serverRequestFn)
.then((response) => {
diff --git a/projects/angular-components/src/lib/th-sortable.directive.spec.ts b/projects/angular-components/src/lib/th-sortable.directive.spec.ts
index 44e6be6..68199d0 100644
--- a/projects/angular-components/src/lib/th-sortable.directive.spec.ts
+++ b/projects/angular-components/src/lib/th-sortable.directive.spec.ts
@@ -2,7 +2,6 @@ import { Component, DebugElement, QueryList, ViewChildren } from '@angular/core'
import { SortEvent, ThSortableDirective, basicTableSort } from './th-sortable.directive';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
-import { NgForOf } from '@angular/common';
@Component({
standalone: true,
@@ -10,19 +9,21 @@ import { NgForOf } from '@angular/common';
- | Name |
- Size |
+ Name |
+ Size |
-
- | {{ item.name }} |
- {{ item.value }} |
-
+ @for (item of items; track item.value) {
+
+ | {{ item.name }} |
+ {{ item.value }} |
+
+ }
`,
- imports: [ThSortableDirective, NgForOf],
+ imports: [ThSortableDirective],
})
class TestComponent {
items = [
@@ -50,13 +51,13 @@ describe('ThSortableDirective', () => {
directiveElements = fixture.debugElement.queryAll(By.directive(ThSortableDirective));
});
- it('on click switches from asc to desc', () => {
+ it('on click switches from ASC to DESC', () => {
directiveElements[0].nativeElement.click();
const directiveInstance = directiveElements[0].injector.get(ThSortableDirective);
- expect(directiveInstance.direction).toBe('asc');
+ expect(directiveInstance.direction).toBe('ASC');
directiveElements[0].nativeElement.click();
- expect(directiveInstance.direction).toBe('desc');
+ expect(directiveInstance.direction).toBe('DESC');
});
it('sorts table rows', () => {
@@ -66,28 +67,28 @@ describe('ThSortableDirective', () => {
const directive1Element = directiveElements[1].nativeElement;
directive0Element.click();
- expect(directive0Instance.direction).toBe('asc');
+ expect(directive0Instance.direction).toBe('ASC');
expect(fixture.componentInstance.items[0]).toEqual({
name: 'a',
value: 2,
});
directive0Element.click();
- expect(directive0Instance.direction).toBe('desc');
+ expect(directive0Instance.direction).toBe('DESC');
expect(fixture.componentInstance.items[0]).toEqual({
name: 'b',
value: 1,
});
directive1Element.click();
- expect(directive1Instance.direction).toBe('asc');
+ expect(directive1Instance.direction).toBe('ASC');
expect(fixture.componentInstance.items[0]).toEqual({
name: 'b',
value: 1,
});
directive1Element.click();
- expect(directive1Instance.direction).toBe('desc');
+ expect(directive1Instance.direction).toBe('DESC');
expect(fixture.componentInstance.items[0]).toEqual({
name: 'a',
value: 2,
diff --git a/projects/angular-components/src/lib/th-sortable.directive.ts b/projects/angular-components/src/lib/th-sortable.directive.ts
index 7653bb4..0cb8e6f 100644
--- a/projects/angular-components/src/lib/th-sortable.directive.ts
+++ b/projects/angular-components/src/lib/th-sortable.directive.ts
@@ -1,6 +1,6 @@
-import { Directive, ElementRef, EventEmitter, HostListener, Input, OnInit, Output, QueryList } from '@angular/core';
+import { Directive, ElementRef, EventEmitter, HostListener, inject, Input, Output, QueryList } from '@angular/core';
-export type SortDirection = 'asc' | 'desc' | null;
+export type SortDirection = 'ASC' | 'DESC' | 'NONE';
export type SortEvent = {
column: string | number;
direction: SortDirection;
@@ -13,8 +13,8 @@ export const anyCompare = (v1: T, v2: T): 1 | -1 | 0 => (v1 < v2 ? -1 : v1 >
export function updateSortableHeaders(headers: QueryList, column: string | number | symbol): void {
for (const header of headers) {
- if (header.sortable !== column) {
- header.updateDirection(null);
+ if (header.columnName !== column) {
+ header.updateDirection('NONE');
}
}
}
@@ -26,11 +26,11 @@ export function basicTableSort>(
): T[] {
updateSortableHeaders(headers, column);
- if (direction == null || column == '') return array;
+ if (direction == 'NONE' || column == '') return array;
return [...array].sort((a, b) => {
const order = compare(a[column], b[column]);
- return direction === 'asc' ? order : -order;
+ return direction === 'ASC' ? order : -order;
});
}
@@ -42,11 +42,11 @@ export function basicAnyValueTableSort(
): T[] {
updateSortableHeaders(headers, column);
- if (direction == null || column == '') return array;
+ if (direction == 'NONE' || column == '') return array;
return [...array].sort((a, b) => {
const order = anyCompare(a[column as keyof T], b[column as keyof T]);
- return direction === 'asc' ? order : -order;
+ return direction === 'ASC' ? order : -order;
});
}
@@ -54,43 +54,29 @@ export function basicAnyValueTableSort(
selector: 'th[sortable]',
standalone: true,
})
-export class ThSortableDirective implements OnInit {
- @Input() sortable: string = '';
- @Input() direction: SortDirection = null;
+export class ThSortableDirective {
+ @Input() columnName: string = '';
+ @Input() direction: SortDirection = 'NONE';
@Output() sorted = new EventEmitter();
- private nextSortOption(sortOption: SortDirection): SortDirection {
- switch (sortOption) {
- case null: {
- return 'asc';
- }
- case 'asc': {
- return 'desc';
- }
- case 'desc': {
- return null;
- }
- default: {
- return sortOption as never;
- }
- }
- }
- private headerText = '';
+ private elementReference: ElementRef = inject(ElementRef);
+ private THElement = this.elementReference.nativeElement;
- constructor(private elementReference: ElementRef) {}
-
- ngOnInit(): void {
- this.headerText = this.elementReference.nativeElement.innerHTML;
+ @HostListener('click') nextSort(): void {
+ this.updateDirection(this.nextSortOption(this.direction));
+ this.sorted.emit({ column: this.columnName, direction: this.direction });
}
updateIcon(direction: SortDirection): void {
- let updateColumnName = '';
- updateColumnName =
- direction == null
- ? this.headerText
- : this.headerText +
- (direction == 'asc' ? ' ' : ' ');
- this.elementReference.nativeElement.innerHTML = updateColumnName;
+ const icon = this.THElement.querySelector('span.sort-icon');
+ if (icon) {
+ icon.remove();
+ }
+ if (direction === 'NONE') return;
+ const iconElement = document.createElement('span');
+ iconElement.classList.add('sort-icon');
+ iconElement.innerHTML = direction == 'ASC' ? '↑' : '↓';
+ this.THElement.append(iconElement);
}
updateDirection(newDirection: SortDirection): void {
@@ -98,8 +84,17 @@ export class ThSortableDirective implements OnInit {
this.updateIcon(this.direction);
}
- @HostListener('click') nextSort(): void {
- this.updateDirection(this.nextSortOption(this.direction));
- this.sorted.emit({ column: this.sortable, direction: this.direction });
+ private nextSortOption(sortOption: SortDirection): SortDirection {
+ switch (sortOption) {
+ case 'NONE': {
+ return 'ASC';
+ }
+ case 'ASC': {
+ return 'DESC';
+ }
+ default: {
+ return 'NONE';
+ }
+ }
}
}
diff --git a/projects/angular-components/src/styles/_dark_theme.scss b/projects/angular-components/src/styles/_dark_theme.scss
index 34a389b..2b6e15d 100644
--- a/projects/angular-components/src/styles/_dark_theme.scss
+++ b/projects/angular-components/src/styles/_dark_theme.scss
@@ -1,19 +1,28 @@
-@use 'variables';
+@use './variables';
ff-button > button {
+ &:hover {
+ color: variables.$ff-color-dark;
+ background-color: variables.$ff-bgcolor-yellow;
+ }
+
&:disabled {
color: variables.$ff-color-gray;
background-color: variables.$ff-bgcolor-dark-gray;
border-color: variables.$ff-bgcolor-dark-gray;
}
+ &:active:not(:disabled) {
+ border: 2px solid variables.$ff-border-yellow;
+ }
+
&.active {
- color: variables.$ff-color-light;
- border-color: variables.$ff-bgcolor-dark-gray;
+ color: variables.$ff-bgcolor-yellow;
+ border-color: variables.$ff-bgcolor-yellow;
&:hover {
color: variables.$ff-color-dark;
- border-color: variables.$ff-bgcolor-yellow;
+ background-color: variables.$ff-bgcolor-yellow;
}
}
}
diff --git a/projects/angular-components/src/styles/_light_theme.scss b/projects/angular-components/src/styles/_light_theme.scss
index af75a2f..45f7c42 100644
--- a/projects/angular-components/src/styles/_light_theme.scss
+++ b/projects/angular-components/src/styles/_light_theme.scss
@@ -1,11 +1,11 @@
-@use 'variables';
+@use './variables';
ff-button *,
ff-search *,
ff-alert *,
ff-chip *,
ff-datatable * {
- font-family: Inter;
+ font-family: Inter, sans-serif;
box-sizing: border-box;
}
@@ -33,9 +33,8 @@ ff-button > button {
&.active {
color: variables.$ff-color-dark;
- background-color: variables.$ff-color-light;
+ background-color: transparent;
border: 2px dashed variables.$ff-border-gray;
- gap: 8px;
&:hover {
color: variables.$ff-color-dark;
@@ -117,3 +116,13 @@ ff-datatable > .dt-wrapper {
border-width: 1px 0;
}
}
+
+th[sortable] {
+ position: relative;
+ cursor: pointer;
+
+ span.sort-icon {
+ position: absolute;
+ right: 0;
+ }
+}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index d8f16f0..4af1d24 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -21,9 +21,9 @@ export class AppComponent implements OnInit {
protected checked2: boolean = true;
protected datasource: DataTableDataSource = new DataTableDataSource();
protected displayedColumns: DataTableColumn[] = [
- { name: 'title', displayName: 'Title', property: 'title' },
- { name: 'description', displayName: 'Description', property: 'description' },
- { name: 'genre', displayName: 'Genre', property: 'genre' },
+ { name: 'title', displayName: 'Title', property: 'title', sortable: true },
+ { name: 'description', displayName: 'Description', property: 'description', sortable: true },
+ { name: 'genre', displayName: 'Genre', property: 'genre', sortable: true },
{ name: 'actions', displayName: 'Actions', property: null, html: true },
{ name: 'something', displayName: 'Something', property: null, html: true },
];
diff --git a/src/styles.scss b/src/styles.scss
index cebaef7..3cc6174 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -1,4 +1,3 @@
-/* You can add global styles to this file, and also import other style files */
@use '../dist/angular-components';
.yellow-dark {
@@ -9,8 +8,6 @@
}
body.ff-dark-theme {
- // @import '../dist/angular-components/styles/dark_theme';
-
color: #fff;
background-color: #1E1E1E;
|