Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion projects/angular-components/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 4 additions & 4 deletions projects/angular-components/src/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,23 @@
<table cdk-table [dataSource]="datasource" class="table table-striped table-hover">
<ng-container *ngFor="let column of displayColumns">
<ng-container [cdkColumnDef]="column.name">
<th cdk-header-cell *cdkHeaderCellDef [className]="column.className" [hidden]="column.hidden">
{{ column.displayName }}
</th>
@if (this.datasource.options.columnSort && !datasource.options.serverSide && column.sortable) {
<th
cdk-header-cell
*cdkHeaderCellDef
sortable
[columnName]="column.name"
(sorted)="onColumnSort($event)"
[className]="column.className"
[hidden]="column.hidden"
>
{{ column.displayName }}
</th>
} @else {
<th cdk-header-cell *cdkHeaderCellDef [className]="column.className" [hidden]="column.hidden">
{{ column.displayName }}
</th>
}
<td cdk-cell *cdkCellDef="let element" [hidden]="column.hidden">
<ng-container *ngIf="!column.html; else htmlBody"
><ng-container>{{ element[column.property] }}</ng-container>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> = {
Expand All @@ -20,6 +32,7 @@ export type DataTableColumn<T> = {
html?: boolean;
className?: string;
hidden?: boolean;
sortable?: boolean;
};

export type DataTableEntryInfo = {
Expand All @@ -37,7 +50,7 @@ export type DataTablePaginationInfo = {
export type DataTableServerRequestInfo = {
size: number;
offset: number;
sort: 'asc' | 'desc';
sort: SortDirection;
};

export type DataTableServerResponseInfo<T> = {
Expand All @@ -56,14 +69,15 @@ type ContentTemplate<T> = {
@Component({
selector: 'ff-datatable',
standalone: true,
imports: [CommonModule, FormsModule, CdkTableModule],
imports: [CommonModule, FormsModule, CdkTableModule, ThSortableDirective],
templateUrl: './datatable.component.html',
styleUrl: './datatable.component.scss',
})
export class DatatableComponent<T> implements AfterViewInit, OnDestroy {
@Input({ required: true }) public datasource!: DataTableDataSource<T>;
@Input({ required: true }) public displayColumns: DataTableColumn<T>[] = [];

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

private datasourceSubscription: Subscription = new Subscription();
private originalData: T[] | null = null;

ngAfterViewInit(): void {
// needed to avoid ExpressionChangedAfterItHasBeenCheckedError
Expand Down Expand Up @@ -105,22 +120,27 @@ export class DatatableComponent<T> 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<T> | 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<T> extends DataSource<T> {
Expand All @@ -132,6 +152,8 @@ export class DataTableDataSource<T> extends DataSource<T> {
size: 50,
filter: true,
serverSide: false,
serverSort: 'NONE',
columnSort: true,
});
private _entriesInfo = new BehaviorSubject<DataTableEntryInfo>({
minPageEntry: 0,
Expand All @@ -147,7 +169,6 @@ export class DataTableDataSource<T> extends DataSource<T> {
private _entriesInfo$ = this._entriesInfo.asObservable();

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

get data(): T[] {
Expand Down Expand Up @@ -237,7 +258,7 @@ export class DataTableDataSource<T> extends DataSource<T> {
Promise.resolve<DataTableServerRequestInfo>({
size: this.options.size,
offset: (this.currentPage - 1) * this.options.size,
sort: 'asc',
sort: this.options.serverSort,
})
.then(this.serverRequestFn)
.then((response) => {
Expand Down
31 changes: 16 additions & 15 deletions projects/angular-components/src/lib/th-sortable.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@ 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,
template: `
<table>
<thead>
<tr>
<th sortable="name" (sorted)="onSort($event)">Name</th>
<th sortable="value" (sorted)="onSort($event)">Size</th>
<th sortColumnName="name" (sorted)="onSort($event)">Name</th>
<th sortColumnName="value" (sorted)="onSort($event)">Size</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of items">
<td>{{ item.name }}</td>
<td>{{ item.value }}</td>
</tr>
@for (item of items; track item.value) {
<tr>
<td>{{ item.name }}</td>
<td>{{ item.value }}</td>
</tr>
}
</tbody>
</table>
`,
imports: [ThSortableDirective, NgForOf],
imports: [ThSortableDirective],
})
class TestComponent {
items = [
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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,
Expand Down
Loading