Skip to content

Commit

Permalink
feat(sort): refactor and add support for multiple columns
Browse files Browse the repository at this point in the history
  • Loading branch information
Hjalmers committed Jan 26, 2023
1 parent e0f0a6e commit e3d2cf2
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 72 deletions.
19 changes: 14 additions & 5 deletions projects/core/src/lib/core.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,20 @@
column.value.class
}}"
[class.disabled]="table.isLoading"
[attr.aria-sort]="sortBy$ | async | sortClass: column.key:true"
[attr.aria-sort]="sortOrder$ | async | sortClass: column.key:'aria'"
[class.gt-sortable]="true"
scope="col"
>
<button
*ngIf="column.value?.sortable"
[attr.data-sort-order]="
sortOrder$ | async | sortClass: column.key:'order'
"
class="gt-sort"
(click)="
table.isLoading || !column.value.sortable || sort(column.key)
table.isLoading ||
!column.value.sortable ||
sortByKey(column.key, $event)
"
>
<span *ngIf="column.value?.header !== false">{{
Expand All @@ -55,12 +60,16 @@
>
<th
class="row-header"
[attr.aria-sort]="sortBy$ | async | sortClass: headerRow.key:true"
[attr.aria-sort]="
sortOrder$ | async | sortClass: headerRow.key:'aria'
"
ngClass="{{ headerRow.value.sortable ? 'sort ' : '' }} {{
sortBy$ | async | sortClass: headerRow.key
sortOrder$ | async | sortClass: headerRow.key
}} {{ (headerRow.key | dashCase) + '-column' }}"
(click)="
table.isLoading || !headerRow.value.sortable || sort(headerRow.key)
table.isLoading ||
!headerRow.value.sortable ||
sortByKey(headerRow.key, $event)
"
scope="col"
>
Expand Down
143 changes: 97 additions & 46 deletions projects/core/src/lib/core.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import {
import {
BehaviorSubject,
combineLatest,
EMPTY,
isObservable,
Observable,
of,
ReplaySubject,
Subject,
} from 'rxjs';
import { TableConfig } from './models/table-config.interface';
import {
Expand All @@ -38,14 +36,19 @@ import {
withLatestFrom,
} from 'rxjs/operators';
import { TableColumn } from './models/table-column.interface';
import { Order } from './enums/order.enum';
import { calculate, chunk, search } from './utilities/utilities';
import {
calculate,
chunk,
search,
sortOnMultipleKeys,
} from './utilities/utilities';
import { TableRow } from './models/table-row.interface';
import { TableSort } from './models/table-sort.interface';
import { GtOrder, GtSortOrder } from './models/table-sort.interface';
import { TableMeta } from './models/table-meta.interface';
import {
GtRowClickEvent,
GtRowHoverEvent,
GtSortEvent,
} from './models/table-events.interface';
import { CapitalCasePipe } from './pipes/capital-case.pipe';
import { SortClassPipe } from './pipes/sort-class.pipe';
Expand Down Expand Up @@ -74,6 +77,10 @@ import { HighlightPipe } from './pipes/highlight.pipe';
],
})
export class CoreComponent {
get sortOrder$(): Observable<GtSortOrder> {
return this._sortOrder$.asObservable();
}

@Input() set loading(isLoading: Observable<boolean> | boolean) {
this._loading$.next(isLoading);
}
Expand All @@ -97,14 +104,25 @@ export class CoreComponent {
this._data$.next(data);
}

@Input() set sortOrder(sortConfig: GtSortOrder<any>) {
if (JSON.stringify(sortConfig) !== JSON.stringify(this._sortOrder$.value)) {
this.sortOrderChange.emit(sortConfig);
this._sortOrder$.next(sortConfig);
}
}

@Output() rowClick = new EventEmitter<GtRowClickEvent>();
@Output('sortOrderChange') sortOrderChange = new EventEmitter<
GtSortOrder<TableRow>
>();

_rowClick(row: TableRow, index: number, event: MouseEvent): void {
this.rowClick.emit({ row, index, event });
}

private _rowHover$ = new ReplaySubject<GtRowHoverEvent>(1);
@Output() rowHover = new EventEmitter<GtRowHoverEvent>();
@Output() columnSort = new EventEmitter<GtSortEvent>();
rowHover$ = this._rowHover$.asObservable().pipe(
debounceTime(50),
distinctUntilChanged((p, q) => p.index === q.index),
Expand Down Expand Up @@ -148,11 +166,8 @@ export class CoreComponent {

private _loading$: ReplaySubject<Observable<boolean> | boolean> =
new ReplaySubject(1);
sortBy$: Subject<TableSort> = new Subject();
// tslint:disable-next-line:variable-name
private _sortBy: TableSort | undefined;

// tslint:disable-next-line:variable-name
private _sortOrder$: BehaviorSubject<GtSortOrder> =
new BehaviorSubject<GtSortOrder>([]);
private _searchBy$: ReplaySubject<Observable<string> | string | null> =
new ReplaySubject(1);
searchBy$: Observable<string | null> = this._searchBy$.pipe(
Expand All @@ -163,9 +178,9 @@ export class CoreComponent {
);

// tslint:disable-next-line:variable-name
private _tableConfig$: ReplaySubject<
private _tableConfig$: BehaviorSubject<
TableConfig<any> | Observable<TableConfig<any>>
> = new ReplaySubject(1);
> = new BehaviorSubject({});
tableConfig$ = this._tableConfig$.pipe(
map((value) => (isObservable(value) ? value : of(value))),
switchMap((obs) => obs),
Expand Down Expand Up @@ -210,35 +225,20 @@ export class CoreComponent {
return { data, config };
}),
switchMap((obs) =>
combineLatest([
of(obs),
this.sortBy$.pipe(startWith(EMPTY)),
this.searchBy$,
])
combineLatest([of(obs), this.sortOrder$, this.searchBy$])
),
map(([table, sortBy, searchBy]) => {
// create a new array reference and sort new array (prevent mutating existing state)
table.data = [...table.data];
return !sortBy
return !sortBy.length || table.config?.disableTableSort
? searchBy
? search(searchBy, false, table.data, table.config)
: table.data
: (searchBy
? search(searchBy, false, table.data, table.config)
: table.data
)?.sort((a, b) => {
// TODO: improve logic
const typed = sortBy as TableSort;
return a[typed.sortBy] > b[typed.sortBy]
? typed.sortByOrder === Order.ASC
? 1
: -1
: b[typed.sortBy] > a[typed.sortBy]
? typed.sortByOrder === Order.ASC
? -1
: 1
: 0;
});
: searchBy
? search(searchBy, false, table.data, table.config)?.sort(
sortOnMultipleKeys(sortBy)
)
: table.data?.sort(sortOnMultipleKeys(sortBy));
}),
shareReplay(1)
);
Expand Down Expand Up @@ -321,19 +321,70 @@ export class CoreComponent {
shareReplay(1)
);

sort(property: string): void {
const newSortOrder =
this._sortBy?.sortBy !== property ||
this._sortBy?.sortByOrder === Order.DESC ||
!this._sortBy.sortByOrder
? Order.ASC
: Order.DESC;
const newSortBy = {
sortBy: property,
sortByOrder: newSortOrder,
/** sortByKey - Sort by key in table row
* @param key - key to sort by
* @param { MouseEvent } [$event] - Mouse event triggering sort, if shift key is pressed sort key will be added to already present sort keys
*/
sortByKey(key: keyof TableRow, $event?: MouseEvent): void {
const shiftKey = $event?.shiftKey;
const currentOrder = this._sortOrder$.value;
let sortOrder: GtOrder = 'asc';
let newOrder: GtSortOrder = [];
// if shift key is pressed while sorting...
if (shiftKey) {
// ...check if key is already sorted
const existingSortPosition = currentOrder.findIndex(
(value) => value.key === key
);
if (existingSortPosition === -1) {
// ...if key is not sorted, add it to the end of the sort order
newOrder = [...currentOrder, { key, order: 'asc' }];
} else {
// ...if key is already sorted, toggle sort order
sortOrder = currentOrder[existingSortPosition].order;
const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
newOrder = [...currentOrder];
newOrder[existingSortPosition] = {
...newOrder[existingSortPosition],
order: newSortOrder,
};
}
} else {
// ...else if shift key is not pressed...
if (currentOrder.length > 0) {
// ...check if key is already sorted
const existingSortPosition = currentOrder.findIndex(
(value) => value.key === key
);
// ...if key is already sorted, toggle sort order
if (existingSortPosition === -1) {
newOrder = [{ key, order: 'asc' }];
} else {
sortOrder = currentOrder[existingSortPosition].order;
const newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
newOrder = [{ key, order: newSortOrder }];
}
} else {
// ...if key is not sorted set sort order for key to ascending
newOrder = [{ key, order: sortOrder }];
}
}
// create sort event
const sortEvent: GtSortEvent = {
key,
order: sortOrder,
currentSortOrder: newOrder,
};
this.sortBy$.next(newSortBy);
this._sortBy = newSortBy;

// if event is passed to sort function...
if ($event) {
// ...emit it as well
sortEvent.event = $event;
}
// emit sort event
this.columnSort.emit(sortEvent);
// update sort order
this.sortOrder = newOrder;
}

columnOrder = (
Expand Down
4 changes: 0 additions & 4 deletions projects/core/src/lib/enums/order.enum.ts

This file was deleted.

3 changes: 2 additions & 1 deletion projects/core/src/lib/models/table-config.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { TableColumn } from './table-column.interface';
import { TableRow } from './table-row.interface';

export interface TableConfig<R = TableRow> {
hidden?: boolean;
/** Disable sorting of table data, useful when sorting is handled externally e.g. server-side pagination. Table will still use sortOrder to visually show how the data is sorted. <p>**Default:** `false`</p> */
disableTableSort?: boolean;
mobileLayout?: boolean;
stickyHeaders?: {
row?: boolean;
Expand Down
8 changes: 8 additions & 0 deletions projects/core/src/lib/models/table-events.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TableRow } from './table-row.interface';
import { GtOrder, GtSortOrder } from './table-sort.interface';

export interface GtRowClickEvent<R = TableRow> {
row: R;
Expand All @@ -11,3 +12,10 @@ export interface GtRowHoverEvent<R = TableRow> {
index: number | null;
event?: MouseEvent;
}

export interface GtSortEvent<R = TableRow> {
key: keyof R;
order: GtOrder;
currentSortOrder: GtSortOrder<R>;
event?: MouseEvent;
}
11 changes: 7 additions & 4 deletions projects/core/src/lib/models/table-sort.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Order } from '../enums/order.enum';
import { TableRow } from './table-row.interface';

export interface TableSort {
sortBy: string;
sortByOrder: Order;
export type GtSortOrder<R = TableRow> = Array<GtSortConfig<R>>;
export interface GtSortConfig<R = {}> {
key: keyof R;
order: GtOrder;
}

export type GtOrder = 'asc' | 'desc';
35 changes: 24 additions & 11 deletions projects/core/src/lib/pipes/sort-class.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Order } from '../enums/order.enum';
import { GtSortOrder } from '../models/table-sort.interface';

@Pipe({
name: 'sortClass',
standalone: true,
})
export class SortClassPipe implements PipeTransform {
transform(
selection: { sortBy: string; sortByOrder: Order } | any,
property: string,
aria = false
sortOrder: GtSortOrder | null,
key: string,
context: 'class' | 'aria' | 'order' = 'class'
): string | null {
return selection?.sortBy === property
? !aria
? 'gt-sort-' + selection.sortByOrder
: selection.sortByOrder + 'ending'
: !aria
? ''
: null;
const sortIndex = sortOrder
? sortOrder.findIndex((s) => s.key === key)
: -1;
if (context === 'aria') {
if (sortIndex === -1 || !sortOrder) {
return null;
} else {
return `${sortOrder[sortIndex].order}ending`;
}
} else if (context === 'class') {
if (sortIndex === -1 || !sortOrder) {
return '';
} else {
return `gt-sort-${sortOrder[sortIndex].order}`;
}
} else {
return (sortOrder && sortOrder?.length === 1) || sortIndex < 0
? null
: sortIndex + 1 + '';
}
}
}
8 changes: 8 additions & 0 deletions projects/core/src/lib/scss/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ $skeleton-loader-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2
align-items: center;
justify-content: inherit;
transition: box-shadow 0.25s ease-in-out;
&[data-sort-order]::before {
content: attr(data-sort-order);
order: 2;
font-variant: diagonal-fractions;
font-weight: normal;
align-self: flex-start;
line-height: initial;
}
&::after {
content: '';
background: $sort-icon-color;
Expand Down
Loading

0 comments on commit e3d2cf2

Please sign in to comment.