finished table
This commit is contained in:
commit
46a84609ed
10 changed files with 297 additions and 119 deletions
|
|
@ -12,16 +12,18 @@ The `SearchInput` component creates a search bar.
|
|||
<script>
|
||||
import { SearchInput } from '@reuters-graphics/graphics-components';
|
||||
|
||||
function handleSearchInput(event) {
|
||||
const searchText = event.detail.value;
|
||||
// Here's where you might update a variable,
|
||||
// filter a dataset or make an API call
|
||||
// based on the user's input.
|
||||
console.log(`Search for ${searchText}`);
|
||||
function filterData(newSearchText: string) {
|
||||
/** This function would typically filter a dataset based on the search input.*/
|
||||
console.log('Filtering data with:', newSearchText);
|
||||
}
|
||||
function handleSearchInput(newSearchText: string) {
|
||||
/** Here's where you might update a variable,
|
||||
filter a dataset or make an API call based on the user's input.*/
|
||||
filterData(newSearchText);
|
||||
}
|
||||
</script>
|
||||
|
||||
<SearchInput on:search={handleSearchInput} />
|
||||
<SearchInput onsearch={handleSearchInput} />
|
||||
```
|
||||
|
||||
<Canvas of={SearchInputStories.Demo} />
|
||||
<Canvas of={SearchInputStories.Demo} />
|
||||
|
|
|
|||
|
|
@ -8,4 +8,17 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<Story name="Demo" />
|
||||
<script lang="ts">
|
||||
function filterData(newSearchText: string) {
|
||||
/** This function would typically filter a dataset based on the search input.*/
|
||||
console.log('Filtering data with:', newSearchText);
|
||||
}
|
||||
function handleSearchInput(newSearchText: string) {
|
||||
/** Here's where you might update a variable, filter a dataset or make an API call based on the user's input.*/
|
||||
filterData(newSearchText);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Story name="Demo">
|
||||
<SearchInput onsearch={handleSearchInput} />
|
||||
</Story>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
interface Props {
|
||||
/** The placeholder text that appears in the search box.*/
|
||||
searchPlaceholder?: string;
|
||||
|
||||
/** Optional function that runs when the input value changes. */
|
||||
onsearch?: (newValue: string) => void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,172 @@ import * as TableStories from './Table.stories.svelte';
|
|||
|
||||
# Table
|
||||
|
||||
The `Table` component TKTK
|
||||
The `Table` component presents data as a table that you can make searchable, filtereable, sortable, or paginated.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Table } from '@reuters-graphics/graphics-components';
|
||||
|
||||
import data from './data.json'; // Import your data
|
||||
</script>
|
||||
|
||||
<Table />
|
||||
<Table {data} />
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.Demo} />
|
||||
|
||||
## Text elements
|
||||
|
||||
Set the `title`, `dek`, `notes` and `source` options to add supporting metadata above and below the table.
|
||||
|
||||
```svelte
|
||||
<Table
|
||||
data={yourData}
|
||||
title='Career home run leaders'
|
||||
dek=`In baseball,
|
||||
a home run (also known as a "dinger" or "tater") occurs when a batter hits the
|
||||
ball over the outfield fence. When a home run is hit, the batter and any runners
|
||||
on base are able to score.`
|
||||
notes='Note: As of Opening Day 2023'
|
||||
source='Source: Baseball Reference'
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.Text} />
|
||||
|
||||
## Truncated
|
||||
|
||||
When your table has 10 or more rows, consider clipping it by setting the `truncated` option. When it is enabled, the table is clipped and readers must click a button below the table to see all rows.
|
||||
|
||||
By default, this configuration will limit the table to 5 records. Change the cutoff point by adjusting the `truncateLength` option.
|
||||
|
||||
This is a good option for simple tables with between 10 and 30 rows. It works best when the table doesn't require interactivity.
|
||||
|
||||
```svelte
|
||||
<Table data={yourData} truncated={true} source="Source: Baseball Reference" />
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.Truncated} />
|
||||
|
||||
## Paginated
|
||||
|
||||
When your table has many rows, you should consider breaking it up into pages by setting `paginated` to `true`. When it is enabled, readers can leaf through the data using a set of buttons below the table.
|
||||
|
||||
By default, there are 25 records per page. Change the number by adjusting the `pageSize` option.
|
||||
|
||||
This is a good option when publishing large tables for readers to explore. It works well with interactive features like searching and filters.
|
||||
|
||||
```svelte
|
||||
<Table
|
||||
data={yourData}
|
||||
paginated={true}
|
||||
title="Press Freedom Index"
|
||||
source="Reporters Without Borders"
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.Paginated} />
|
||||
|
||||
## Search bar
|
||||
|
||||
Allow users to search the table by setting the optional `searchable` option to `true`. Modify the default text that appears in the box by setting `searchPlaceholder` to a different placeholder text.
|
||||
|
||||
```svelte
|
||||
<Table
|
||||
data={yourData}
|
||||
searchable={true}
|
||||
paginated={true}
|
||||
searchPlaceholder="Search press freedom data"
|
||||
,
|
||||
title="Press Freedom Index"
|
||||
notes="Source: Reporters Without Borders"
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.Search} />
|
||||
|
||||
## Filter
|
||||
|
||||
Allow users to filter the table by providing one of the attributes as the `filterField`. This works best with categorical columns.
|
||||
|
||||
Set `filterLabel` to make the category name more readable. For example, if the column is `Region`, set `filterLabel` to `regions` or `regions of the world`.
|
||||
|
||||
```svelte
|
||||
<Table
|
||||
data={yourData}
|
||||
filterField="Region"
|
||||
filterLabel="regions"
|
||||
paginated={true}
|
||||
title="Press Freedom Index"
|
||||
notes="Source: Reporters Without Borders"
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.Filter} />
|
||||
|
||||
## Search and filter
|
||||
|
||||
Feel free to both search and filter.
|
||||
|
||||
```svelte
|
||||
<Table
|
||||
data={yourData}
|
||||
searchable={true}
|
||||
paginated={true}
|
||||
filterField="Region"
|
||||
filterLabel="regions"
|
||||
title="Press Freedom Index"
|
||||
dek="Reporters Without Borders ranks countries based on their level of press freedom using criteria such as the degree of media pluralism and violence against journalists."
|
||||
source="Source: Reporters Without Borders"
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.SearchAndFilter} />
|
||||
```
|
||||
|
||||
## Sort
|
||||
|
||||
Allow users to sort the table by setting `sortable` to `true`. Specify the starting order by setting `sortField` to a column name and `sortDirection` to `ascending` or `descending`.
|
||||
|
||||
By default, all fields are sortable. If you'd like to limit the columns where sorting is allowed, provide a list to the `sortableFields` option.
|
||||
|
||||
```svelte
|
||||
<Table
|
||||
data={yourData}
|
||||
sortable={true}
|
||||
paginated={true}
|
||||
sortField="Score"
|
||||
sortDirection="descending"
|
||||
title="Press Freedom Index"
|
||||
source="Source: Reporters Without Borders"
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.Sort} />
|
||||
|
||||
## Format
|
||||
|
||||
Format column values by supplying functions keyed to field names with the `fieldFormatters` option. Columns are still sorted using the raw, underlying values.
|
||||
|
||||
Among other things, this feature can be used to provide a unit of measurement, such as `$` or `%`, with numeric fields.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
const fieldFormatters = {
|
||||
// The key must match the column name in the data exactly
|
||||
'Net worth (in billions)': (v) => '$' + v.toFixed(1),
|
||||
};
|
||||
</script>
|
||||
|
||||
<Table
|
||||
data={yourData}
|
||||
{fieldFormatters}
|
||||
sortable={true}
|
||||
sortField="Net worth (in billions)"
|
||||
sortDirection="descending"
|
||||
title="The Richest Women in the World"
|
||||
source="Source: Forbes"
|
||||
/>
|
||||
```
|
||||
|
||||
<Canvas of={TableStories.Format} />
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@
|
|||
import pressFreedom from './demo/pressFreedom.json';
|
||||
import homeRuns from './demo/homeRuns.json';
|
||||
import richestWomen from './demo/richestWomen.json';
|
||||
import type { Formatter } from './utils';
|
||||
|
||||
const currencyFormat = (v: number) => '$' + v.toFixed(1);
|
||||
const currencyFormat: Formatter<number> = (v: number) => '$' + v.toFixed(1);
|
||||
</script>
|
||||
|
||||
<Story
|
||||
|
|
@ -31,9 +32,9 @@
|
|||
/>
|
||||
|
||||
<Story
|
||||
name="Metadata"
|
||||
name="Text elements"
|
||||
exportName="Text"
|
||||
args={{
|
||||
width: 'normal',
|
||||
data: homeRuns,
|
||||
title: 'Career home run leaders',
|
||||
dek: 'In baseball, a home run (also known as a "dinger" or "tater") occurs when a batter hits the ball over the outfield fence. When a home run is hit, the batter and any runners on base are able to score.',
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
/>
|
||||
|
||||
<Story
|
||||
name="Truncate"
|
||||
name="Truncated"
|
||||
args={{
|
||||
data: homeRuns,
|
||||
truncated: true,
|
||||
|
|
@ -52,21 +53,23 @@
|
|||
/>
|
||||
|
||||
<Story
|
||||
name="Paginate"
|
||||
name="Paginated"
|
||||
args={{
|
||||
data: pressFreedom,
|
||||
title: 'Press Freedom Index',
|
||||
paginated: true,
|
||||
title: 'Press Freedom Index',
|
||||
source: 'Source: Reporters Without Borders',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="Search"
|
||||
name="Search bar"
|
||||
exportName="Search"
|
||||
args={{
|
||||
data: pressFreedom,
|
||||
searchable: true,
|
||||
paginated: true,
|
||||
searchPlaceholder: 'Search press freedom data',
|
||||
title: 'Press Freedom Index',
|
||||
source: 'Source: Reporters Without Borders',
|
||||
}}
|
||||
|
|
@ -78,6 +81,7 @@
|
|||
data: pressFreedom,
|
||||
paginated: true,
|
||||
filterField: 'Region',
|
||||
filterLabel: 'regions',
|
||||
title: 'Press Freedom Index',
|
||||
notes: 'Source: Reporters Without Borders',
|
||||
}}
|
||||
|
|
@ -85,11 +89,13 @@
|
|||
|
||||
<Story
|
||||
name="Search and filter"
|
||||
exportName="SearchAndFilter"
|
||||
args={{
|
||||
data: pressFreedom,
|
||||
searchable: true,
|
||||
filterField: 'Region',
|
||||
paginated: true,
|
||||
filterField: 'Region',
|
||||
filterLabel: 'regions',
|
||||
title: 'Press Freedom Index',
|
||||
dek: 'Reporters Without Borders ranks countries based on their level of press freedom using criteria such as the degree of media pluralism and violence against journalists.',
|
||||
source: 'Source: Reporters Without Borders',
|
||||
|
|
@ -101,9 +107,9 @@
|
|||
args={{
|
||||
data: pressFreedom,
|
||||
sortable: true,
|
||||
paginated: true,
|
||||
sortField: 'Score',
|
||||
sortDirection: 'descending',
|
||||
paginated: true,
|
||||
title: 'Press Freedom Index',
|
||||
notes: 'Note: data as of 2018',
|
||||
source: 'Source: Reporters Without Borders',
|
||||
|
|
@ -114,20 +120,12 @@
|
|||
name="Format"
|
||||
args={{
|
||||
data: richestWomen,
|
||||
title: 'The Richest Women in the World',
|
||||
source: 'Source: Forbes',
|
||||
fieldFormatters: {
|
||||
'Net worth (in billions)': currencyFormat as Formatter<unknown>,
|
||||
},
|
||||
sortable: true,
|
||||
sortField: 'Net worth (in billions)',
|
||||
sortDirection: 'descending',
|
||||
fieldFormatters: { 'Net worth (in billions)': currencyFormat },
|
||||
}}
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="Style"
|
||||
args={{
|
||||
id: 'custom-table',
|
||||
data: richestWomen,
|
||||
title: 'The Richest Women in the World',
|
||||
source: 'Source: Forbes',
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,17 @@
|
|||
import Select from './components/Select.svelte';
|
||||
import SortArrow from './components/SortArrow.svelte';
|
||||
import SearchInput from '../SearchInput/SearchInput.svelte';
|
||||
import { filterArray, paginateArray, getOptions } from './utils';
|
||||
import {
|
||||
filterArray,
|
||||
paginateArray,
|
||||
getOptions,
|
||||
sortArray,
|
||||
formatValue,
|
||||
type FieldFormatters,
|
||||
} from './utils';
|
||||
|
||||
// Types
|
||||
import type { Option } from '$lib/components/@types/global';
|
||||
|
||||
interface Props<T extends Record<string, unknown>> {
|
||||
/** Data for the table as an array. */
|
||||
|
|
@ -47,7 +57,7 @@
|
|||
/** The direction of the sort. By default it's ascending. */
|
||||
sortDirection?: 'ascending' | 'descending';
|
||||
/** Custom field formatting functions. Should be keyed to the name of the field. */
|
||||
fieldFormatters?: object;
|
||||
fieldFormatters?: FieldFormatters<T>;
|
||||
/** Width of the component within the text well. */
|
||||
width?: 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
|
||||
/** Add an ID to target with SCSS. */
|
||||
|
|
@ -75,33 +85,12 @@
|
|||
sortField = Object.keys(data[0])[0],
|
||||
sortableFields = Object.keys(data[0]).filter((f) => f !== 'searchStr'),
|
||||
sortDirection = $bindable('ascending'),
|
||||
fieldFormatters = {},
|
||||
fieldFormatters,
|
||||
width = 'normal',
|
||||
id = '',
|
||||
class: cls = '',
|
||||
}: Props<Record<string, unknown>> = $props();
|
||||
|
||||
/** Derived variables */
|
||||
// let includedFieldsDerived = $derived.by(() => {
|
||||
// if (includedFields) return includedFields;
|
||||
// if (data.length > 0)
|
||||
// return Object.keys(data[0]).filter((f) => f !== 'searchStr');
|
||||
// return [];
|
||||
// });
|
||||
|
||||
// let sortableFieldsDerived = $derived.by(() => {
|
||||
// if (sortableFields) return sortableFields;
|
||||
// if (data.length > 0)
|
||||
// return Object.keys(data[0]).filter((f) => f !== 'searchStr');
|
||||
// return [];
|
||||
// });
|
||||
|
||||
// let sortFieldDerived = $derived.by(() => {
|
||||
// if (sortField) return sortField;
|
||||
// if (data.length > 0) return Object.keys(data[0])[0];
|
||||
// return '';
|
||||
// });
|
||||
|
||||
/** Set truncate, filtering and pagination configuration */
|
||||
let showAll = $state(false);
|
||||
let pageNumber = $state(1);
|
||||
|
|
@ -109,61 +98,31 @@
|
|||
let filterList = $derived(
|
||||
filterField ? getOptions(data, filterField) : undefined
|
||||
);
|
||||
let filterValue = '';
|
||||
let filterValue = $state('');
|
||||
|
||||
/** Helper functions that modify variables within this component */
|
||||
//* * Handle show all, search, filter, sort and pagination events */
|
||||
function toggleTruncate(_event) {
|
||||
function toggleTruncate() {
|
||||
showAll = !showAll;
|
||||
}
|
||||
|
||||
// function handleSearchInput(event: CustomEvent<string>) {
|
||||
// searchText = event.detail;
|
||||
|
||||
// console.log('searchText', searchText);
|
||||
// pageNumber = 1;
|
||||
// }
|
||||
|
||||
/** Filters table data based on the input value in the search bar */
|
||||
function handleSearchInput(newSearchText: string) {
|
||||
searchText = newSearchText;
|
||||
pageNumber = 1;
|
||||
}
|
||||
|
||||
function handleFilterInput(event) {
|
||||
const value = event.detail.value;
|
||||
filterValue = value === 'All' ? '' : value;
|
||||
function handleFilterInput(newSearchText: string) {
|
||||
filterValue = newSearchText === 'All' ? '' : newSearchText;
|
||||
pageNumber = 1;
|
||||
}
|
||||
|
||||
function handleSort(event) {
|
||||
function handleSort(event: MouseEvent) {
|
||||
if (!sortable) return;
|
||||
sortField = event.target.getAttribute('data-field');
|
||||
sortField = (event.target as HTMLElement).getAttribute('data-field') || '';
|
||||
sortDirection = sortDirection === 'ascending' ? 'descending' : 'ascending';
|
||||
}
|
||||
|
||||
function sortArray(array, column, direction) {
|
||||
if (!sortable) return array;
|
||||
return array.sort((a, b) => {
|
||||
if (a[column] < b[column]) {
|
||||
return direction === 'ascending' ? -1 : 1;
|
||||
} else if (a[column] > b[column]) {
|
||||
return direction === 'ascending' ? 1 : -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatValue(item, field) {
|
||||
const value = item[field];
|
||||
if (field in fieldFormatters) {
|
||||
const func = fieldFormatters[field];
|
||||
return func(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Add the `searchStr` field to data */
|
||||
let searchableData = $derived.by(() => {
|
||||
return data.map((d) => {
|
||||
|
|
@ -182,7 +141,7 @@
|
|||
);
|
||||
|
||||
let sortedData = $derived.by(() =>
|
||||
sortArray(filteredData, sortField, sortDirection)
|
||||
sortArray(filteredData, sortField, sortDirection, sortable)
|
||||
);
|
||||
|
||||
let currentPageData = $derived.by(() => {
|
||||
|
|
@ -212,8 +171,8 @@
|
|||
<div class="table--header--filter">
|
||||
<Select
|
||||
label={filterLabel || filterField}
|
||||
options={filterList}
|
||||
on:select={handleFilterInput}
|
||||
options={filterList as Option[]}
|
||||
onselect={handleFilterInput}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -270,7 +229,7 @@
|
|||
data-field={field}
|
||||
data-value={item[field]}
|
||||
>
|
||||
{@html formatValue(item, field)}
|
||||
{@html formatValue(item, field, fieldFormatters)}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -6,22 +6,18 @@
|
|||
interface Props {
|
||||
/**
|
||||
* The current page number.
|
||||
* @type {number}
|
||||
*/
|
||||
pageNumber?: number;
|
||||
/**
|
||||
* The default page size.
|
||||
* @type {number}
|
||||
*/
|
||||
pageSize?: number;
|
||||
/**
|
||||
* The number of records in the current page.
|
||||
* @type {number}
|
||||
*/
|
||||
pageLength?: number;
|
||||
/**
|
||||
* The total number of records in the data set.
|
||||
* @type {number}
|
||||
*/
|
||||
n?: number;
|
||||
}
|
||||
|
|
@ -29,8 +25,8 @@
|
|||
let {
|
||||
pageNumber = $bindable(1),
|
||||
pageSize = $bindable(25),
|
||||
pageLength = $bindable(null),
|
||||
n = $bindable(null),
|
||||
pageLength = $bindable(1),
|
||||
n = $bindable(1),
|
||||
}: Props = $props();
|
||||
|
||||
let minRow = $derived(pageNumber * pageSize - pageSize + 1);
|
||||
|
|
|
|||
|
|
@ -1,42 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Option } from '../@types/global';
|
||||
import type { Option } from '$lib/components/@types/global';
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The label that appears above the select input.
|
||||
* @type {string}
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* The label that appears above the select input.
|
||||
* @type {Array}
|
||||
*/
|
||||
options?: Option[];
|
||||
/** Optional function that runs when the selected value changes. */
|
||||
onselect?: (newValue: string) => void;
|
||||
}
|
||||
|
||||
let { label = '', options = [] }: Props = $props();
|
||||
let { label = '', options = [], onselect }: Props = $props();
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function input(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
|
||||
function input(event) {
|
||||
const value = event.target.value;
|
||||
dispatch('select', { value });
|
||||
if (onselect) onselect(value); // Call the prop to update the parent when selected
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="select">
|
||||
{#if label}
|
||||
<label class="body-caption block" for="select--input">{label}</label>
|
||||
{/if}
|
||||
<select
|
||||
class="select--input body-caption fpx-2"
|
||||
name="select--input"
|
||||
id="select--input"
|
||||
oninput="{input}"
|
||||
oninput={input}
|
||||
>
|
||||
{#each options as obj}
|
||||
<option value="{obj.value}">{obj.text}</option>
|
||||
<option value={obj.value}>{obj.text} {label.toLowerCase()}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@
|
|||
class="avoid-clicks"
|
||||
>
|
||||
<path
|
||||
class:active="{sortDirection === 'descending' && active}"
|
||||
class:active={sortDirection === 'descending' && active}
|
||||
d="M6.76474 20.2244L0.236082 13.4649C-0.0786943 13.139 -0.0786943 12.6104 0.236082 12.2845C0.550521 11.959 1.19794 11.96 1.51305 12.2845L7.33483 12.2845L13 12.2845C13.43 11.8545 14.1195 11.9593 14.4339 12.2849C14.7487 12.6107 14.7487 13.1394 14.4339 13.4653L7.90492 20.2244C7.59015 20.5503 7.07952 20.5503 6.76474 20.2244Z"
|
||||
></path>
|
||||
<path
|
||||
class:active="{sortDirection === 'ascending' && active}"
|
||||
class:active={sortDirection === 'ascending' && active}
|
||||
d="M7.90518 0.244414L14.4338 7.00385C14.7486 7.32973 14.7486 7.85838 14.4338 8.18427C14.1194 8.50981 13.472 8.50876 13.1569 8.18427L7.33509 8.18427L1.66992 8.18427C1.23992 8.61427 0.550443 8.50946 0.236003 8.18392C-0.0787725 7.85803 -0.0787725 7.32938 0.236003 7.0035L6.765 0.244414C7.07978 -0.0814713 7.5904 -0.0814713 7.90518 0.244414Z"
|
||||
></path>
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -63,3 +63,60 @@ export function getOptions<T>(data: T[], attr: keyof T) {
|
|||
// Convert the list into Option typed objects ready for our Select component
|
||||
return attrList.map((a) => ({ text: a, value: a }));
|
||||
}
|
||||
interface SortableItem {
|
||||
[key: string]: unknown; // Or more specific types if known
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts an array of objects based on a specified column and direction.
|
||||
*/
|
||||
export function sortArray<T extends SortableItem>(
|
||||
/** The array to sort. */
|
||||
array: T[],
|
||||
/** The column to sort by. */
|
||||
column: keyof T,
|
||||
/** The sorting direction ('ascending' or 'descending'). */
|
||||
direction: 'ascending' | 'descending',
|
||||
/** Whether or not sorting is turned on */
|
||||
sortable: boolean
|
||||
) {
|
||||
if (!sortable) return array;
|
||||
|
||||
const sorted = [...array].sort((a, b) => {
|
||||
if (a[column] < b[column]) {
|
||||
return direction === 'ascending' ? -1 : 1;
|
||||
} else if (a[column] > b[column]) {
|
||||
return direction === 'ascending' ? 1 : -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export type Formatter<T> = (value: T) => string;
|
||||
|
||||
export type FieldFormatters<T> = {
|
||||
[K in keyof T]?: Formatter<T[K]>;
|
||||
};
|
||||
/**
|
||||
* Formats a value based on a field and a dictionary of formatters.
|
||||
*/
|
||||
export function formatValue<T extends Record<string, unknown>>(
|
||||
/** The object containing the field. */
|
||||
item: FilterableDatum<T>,
|
||||
/** The field to format. */
|
||||
field: keyof T,
|
||||
/** An optional dictionary of formatters. */
|
||||
fieldFormatters?: FieldFormatters<T>
|
||||
) {
|
||||
const value = item[field];
|
||||
|
||||
if (fieldFormatters && field in fieldFormatters && fieldFormatters[field]) {
|
||||
const formatter = fieldFormatters[field];
|
||||
return formatter(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue