diff --git a/README.md b/README.md index 59060c4..de0774f 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,14 @@ A collection of reusable components designed for use in Frank!Framework projects ![frank-framework-github-banner](banner.png) ## Available Components -| Component | Selector | Description -| --- | --- | --- -| [Alert](/projects/angular-components/src/lib/alert/) | <ff-alert> | Alert the user, useful for forms, documentation or to give a warning for anything. -| [Button](/projects/angular-components/src/lib/button/) | <ff-button> | Buttons that fit the FF style & can have a toggleable active state -| [Chip](/projects/angular-components/src/lib/chip/) | <ff-chip> | A stylized border around a word or short text, most likely used for labeling -| [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 -| [Checkbox](/projects/angular-components/src/lib/checkbox/) | <ff-checkbox> | A custom checkbox using the ff colourscheme +| Component | Selector | Description | +|--------------------------------------------------------------|----------------------|--------------------------------------------------------------------------------------| +| [Alert](/projects/angular-components/src/lib/alert/) | <ff-alert> | Alert the user, useful for forms, documentation or to give a warning for anything | +| [Button](/projects/angular-components/src/lib/button/) | <ff-button> | Buttons that fit the FF style & can have a toggleable active state | +| [Chip](/projects/angular-components/src/lib/chip/) | <ff-chip> | A stylized border around a word or short text, most likely used for labeling | +| [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 | +| [Checkbox](/projects/angular-components/src/lib/checkbox/) | <ff-checkbox> | A custom checkbox using the ff colourscheme | +| [Datatable](/projects/angular-components/src/lib/datatable/) | <ff-datatable> | Datatable that is able to handle ng templates & server side data | ## How to use Install the package from NPM (coming soon) diff --git a/projects/angular-components/package.json b/projects/angular-components/package.json index 8a3bf3d..e8d6098 100644 --- a/projects/angular-components/package.json +++ b/projects/angular-components/package.json @@ -1,6 +1,6 @@ { "name": "@frankframework/angular-components", - "version": "1.1.7", + "version": "1.2.0", "description": "A collection of reusable components designed for use in Frank!Framework projects", "main": "", "author": "Vivy Booman", diff --git a/projects/angular-components/src/_index.scss b/projects/angular-components/src/_index.scss index e311598..ba6826c 100644 --- a/projects/angular-components/src/_index.scss +++ b/projects/angular-components/src/_index.scss @@ -3,17 +3,17 @@ @font-face { font-family: 'Inter'; - font-stretch: normal; font-style: normal; - font-weight: 100, 200, 300, 400, 500, 600, 700, 800, 900; + font-weight: 100 900; + font-display: swap; src: url('./assets/Inter-VariableFont_opsz,wght.ttf') format('truetype'); } @font-face { font-family: 'Inter'; - font-stretch: normal; font-style: italic; - font-weight: 100, 200, 300, 400, 500, 600, 700, 800, 900; + font-weight: 100 900; + font-display: swap; src: url('./assets/Inter-Italic-VariableFont_opsz,wght.ttf') format('truetype'); } diff --git a/projects/angular-components/src/lib/datatable/datatable.component.html b/projects/angular-components/src/lib/datatable/datatable.component.html index 112deaf..56ea1c6 100644 --- a/projects/angular-components/src/lib/datatable/datatable.component.html +++ b/projects/angular-components/src/lib/datatable/datatable.component.html @@ -21,9 +21,23 @@ - + @if (this.datasource.options.columnSort && !datasource.options.serverSide && column.sortable) { + + } @else { + + }
- {{ column.displayName }} - + {{ column.displayName }} + + {{ 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'; - - + + - - - - + @for (item of items; track item.value) { + + + + + }
NameSizeNameSize
{{ item.name }}{{ 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;