-
Notifications
You must be signed in to change notification settings - Fork 273
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web-core): new useTable composable (#7936)
- Loading branch information
Showing
10 changed files
with
515 additions
and
0 deletions.
There are no files selected for viewing
159 changes: 159 additions & 0 deletions
159
@xen-orchestra/web-core/docs/composables/table.composable.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
# `useTable` composable | ||
|
||
## Usage | ||
|
||
```ts | ||
const { columns, visibleColumns, rows, columnsById } = useTable('<table-id>', records, { | ||
rowId: record => record.id, | ||
columns: define => [ | ||
define('<column-id>', { label: 'Column 1' }), | ||
define('<column-id>', { label: 'Column 2' }), | ||
define('<column-id>', { label: 'Column 3' }), | ||
], | ||
}) | ||
``` | ||
|
||
## `useTable` options | ||
|
||
| Name | Type | Required | Description | | ||
| --------- | ---------------------------------------------- | :------: | ------------------------------------------------- | | ||
| `rowId` | `(record: TRecord) => string` | ✓ | A function that define the id of a row. | | ||
| `columns` | `(define: DefineColumn) => ColumnDefinition[]` | ✓ | A function that defines the columns of the table. | | ||
|
||
## Defining a column | ||
|
||
```ts | ||
define('<TColumnId>', options) // TValue will be TRecord[TColumnId] | ||
define('<TColumnId>', `<TProperty>`, options) // TValue will be TRecord[TProperty] | ||
define('<TColumnId>', (record: TRecord) => '<TValue>', options) // TValue will be the result of the function | ||
``` | ||
|
||
### Column options | ||
|
||
| Name | Type | Required | Default | Description | | ||
| ------------ | ---------------------------------- | :------: | ------- | -------------------------------------- | | ||
| `label` | `string` | ✓ | | The column label. | | ||
| `isHideable` | `boolean` | | `true` | Indicates if the column can be hidden. | | ||
| `compareFn` | `(a: TValue, b: TValue) => number` | | | A function used to compare the values. | | ||
|
||
## `columns` | ||
|
||
An array containing all columns defined in the table. | ||
|
||
### Properties of a column | ||
|
||
| Name | Type | Description | | ||
| ------------ | ----------------------------- | ------------------------------------------------ | | ||
| `id` | `string` | The column id. | | ||
| `label` | `string` | The column label. | | ||
| `isVisible` | `boolean` | Indicates if the column is visible. | | ||
| `getter` | `(record: TRecord) => TValue` | A function that returns the value of the column. | | ||
| `isSortable` | `boolean` | Indicates if the column is sortable. | | ||
| `isHideable` | `boolean` | Indicates if the column is hideable. | | ||
|
||
#### If `isSortable` is `true` | ||
|
||
| Name | Type | Description | | ||
| --------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------- | | ||
| `compareFn` | `(a: TValue, b: TValue) => number` | The compare function defined in the column options. | | ||
| `isSorted` | `boolean` | Indicates if the column is sorted. | | ||
| `isSortedAsc` | `boolean` | Indicates if the column is sorted in ascending order. | | ||
| `isSortedDesc` | `boolean` | Indicates if the column is sorted in descending order. | | ||
| `sort` | `(direction, toggleOffIfSameDirection?) => void` | A function that sorts the rows based on the column values. | | ||
| ⤷ `direction` | `'asc' \| 'desc' \| false` | The sort direction. If `false`, the column is unsorted. | | ||
| ⤷ `toggleOffIfSameDirection` | `boolean` | Indicates if the column should be unsorted if it is already sorted in the same direction. | | ||
| `sortAsc` | `(toggleOffIfSameDirection?) => void` | A function that sorts the rows based on the column values in ascending order. | | ||
| ⤷ `toggleOffIfSameDirection` | `boolean` | Indicates if the column should be unsorted if it is already sorted in ascending order. | | ||
| `sortDesc` | `(toggleOffIfSameDirection?) => void` | A function that sorts the rows based on the column values in descending order. | | ||
| ⤷ `toggleOffIfSameDirection` | `boolean` | Indicates if the column should be unsorted if it is already sorted in descending order. | | ||
|
||
#### If `isHideable` is `true` | ||
|
||
| Name | Type | Description | | ||
| -------------------- | --------------------------- | ------------------------------------------------------------------------------- | | ||
| `hide` | `() => void` | A function that hides the column. | | ||
| `show` | `() => void` | A function that shows the column. | | ||
| `toggle` | `(value?: boolean) => void` | A function that toggles the visibility of the column. | | ||
| ⤷ `value` | `boolean \| undefined` | If undefined, the visibility will be toggled. Else it will be set to the value. | | ||
|
||
## `visibleColumns` | ||
|
||
Same as `columns` but only contains the visible columns. | ||
|
||
## `rows` | ||
|
||
An array containing all rows of the table. | ||
|
||
### Properties of a row | ||
|
||
| Name | Type | Description | | ||
| ---------------- | ---------- | ------------------------------- | | ||
| `id` | `string` | The row id. | | ||
| `value` | `TRecord` | The record of the row. | | ||
| `visibleColumns` | `Column[]` | The visible columns of the row. | | ||
|
||
#### `visibleColumns` | ||
|
||
An array containing the visible columns of the row. | ||
|
||
##### Properties of a row column | ||
|
||
| Name | Type | Description | | ||
| ------- | -------- | ------------------------ | | ||
| `id` | `string` | The column id. | | ||
| `value` | `TValue` | The value of the column. | | ||
|
||
## `columnsById` | ||
|
||
An object containing all columns defined in the table indexed by their id. | ||
|
||
## Example | ||
|
||
```vue | ||
<template> | ||
<div> | ||
<button v-for="column of columns" :key="column.id" @click.prevent="column.toggle()"> | ||
{{ column.isVisible ? 'Hide' : 'Show' }} {{ column.label }} | ||
</button> | ||
</div> | ||
<table> | ||
<thead> | ||
<tr> | ||
<th v-for="column of visibleColumns" :key="column.id"> | ||
{{ column.label }} | ||
<button v-if="column.isHideable" @click.prevent="column.hide()">Hide</button> | ||
<template v-if="column.isSortable"> | ||
<button @click.prevent="column.sortAsc(true)">Sort ASC</button> | ||
<button @click.prevent="column.sortDesc(true)">Sort DESC</button> | ||
</template> | ||
</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<tr v-for="row of rows" :key="row.id"> | ||
<td v-for="column of row.visibleColumns" :key="column.id"> | ||
{{ column.value }} | ||
</td> | ||
</tr> | ||
</tbody> | ||
</table> | ||
</template> | ||
<script lang="ts" setup> | ||
const { columns, visibleColumns, rows } = useTable( | ||
'users', | ||
[ | ||
{ id: 1, name: 'John', age: 25 }, | ||
{ id: 2, name: 'Jane', age: 30 }, | ||
{ id: 3, name: 'Alice', age: 20 }, | ||
], | ||
{ | ||
rowId: record => record.id, | ||
columns: define => [ | ||
define('name', { label: 'Name', isHideable: false }), | ||
define('age', { label: 'Age', compareFn: (user1, user2) => user1.age - user2.age }), | ||
], | ||
} | ||
) | ||
</script> | ||
``` |
10 changes: 10 additions & 0 deletions
10
@xen-orchestra/web-core/lib/composables/hide-route-query.composable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { useRouteQuery } from '@core/composables/route-query.composable' | ||
|
||
export function useHideRouteQuery(id: string) { | ||
return useRouteQuery(id, { | ||
toData: query => new Set(query ? query.split(',') : undefined), | ||
toQuery: data => Array.from(data).join(','), | ||
}) | ||
} | ||
|
||
export type HideRouteQuery = ReturnType<typeof useHideRouteQuery> |
18 changes: 18 additions & 0 deletions
18
@xen-orchestra/web-core/lib/composables/sort-route-query.composable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { useRouteQuery } from '@core/composables/route-query.composable' | ||
|
||
export function useSortRouteQuery(id: string) { | ||
return useRouteQuery(id, { | ||
toData: query => { | ||
if (!query) { | ||
return undefined | ||
} | ||
|
||
const [id, direction] = query.split(',') as [string, 'asc' | 'desc'] | ||
|
||
return { id, direction } | ||
}, | ||
toQuery: data => (data ? [data.id, data.direction].join(',') : ''), | ||
}) | ||
} | ||
|
||
export type SortRouteQuery = ReturnType<typeof useSortRouteQuery> |
76 changes: 76 additions & 0 deletions
76
@xen-orchestra/web-core/lib/composables/table.composable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { useHideRouteQuery } from '@core/composables/hide-route-query.composable' | ||
import { useSortRouteQuery } from '@core/composables/sort-route-query.composable' | ||
import { createDefineColumn } from '@core/composables/table/create-define-column' | ||
import type { ColumnDefinition, Table, TableOptions } from '@core/composables/table/type' | ||
import type { MaybeRefOrGetter } from 'vue' | ||
import { computed, reactive, toValue } from 'vue' | ||
|
||
export function useTable<TRecord, TRowId, const TDefinitions extends ColumnDefinition<any, TRecord, any, any, any>>( | ||
id: string, | ||
records: MaybeRefOrGetter<TRecord[]>, | ||
options: TableOptions<TRecord, TRowId, TDefinitions> | ||
): Table<TRowId, TDefinitions[]> { | ||
const hideRouteQuery = useHideRouteQuery(`table.${id}.hide`) | ||
|
||
const sortRouteQuery = useSortRouteQuery(`table.${id}.sort`) | ||
|
||
const defineColumn = createDefineColumn<TRecord>(hideRouteQuery, sortRouteQuery) | ||
|
||
const columns = options.columns(defineColumn) | ||
|
||
const columnsById = Object.fromEntries(columns.map(column => [column.id, column])) as Record< | ||
string, | ||
ColumnDefinition<any, TRecord, any, any, any> | ||
> | ||
|
||
const visibleColumns = computed(() => columns.filter(column => column.isVisible)) | ||
|
||
const rows = computed(() => | ||
toValue(records).map(record => { | ||
const rowId = options.rowId(record) | ||
|
||
const visibleRowColumns = computed(() => | ||
visibleColumns.value.map(column => ({ | ||
id: column.id, | ||
value: column.getter(record), | ||
})) | ||
) | ||
|
||
return reactive({ | ||
id: rowId, | ||
value: record, | ||
visibleColumns: visibleRowColumns, | ||
}) | ||
}) | ||
) | ||
|
||
const sortedRows = computed(() => { | ||
const sort = sortRouteQuery.value | ||
|
||
if (sort === undefined) { | ||
return rows.value | ||
} | ||
|
||
const sortColumn = columnsById[sort.id] | ||
|
||
if (sortColumn === undefined || !sortColumn.isSortable) { | ||
return rows.value | ||
} | ||
|
||
const compareFn = sortColumn.compareFn | ||
|
||
return rows.value.slice().sort((row1, row2) => { | ||
const value1 = sortColumn.getter(row1.value as TRecord) | ||
const value2 = sortColumn.getter(row2.value as TRecord) | ||
|
||
return sort.direction === 'asc' ? compareFn(value1, value2) : compareFn(value2, value1) | ||
}) | ||
}) | ||
|
||
return { | ||
columns: computed(() => columns), | ||
columnsById: computed(() => columnsById), | ||
visibleColumns, | ||
rows: sortedRows, | ||
} as Table<TRowId, TDefinitions[]> | ||
} |
20 changes: 20 additions & 0 deletions
20
@xen-orchestra/web-core/lib/composables/table/create-base-definition.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import type { BaseDefinition, ColumnOptions } from '@core/composables/table/type' | ||
|
||
export function createBaseDefinition<TId extends string, TRecord>( | ||
columnId: TId, | ||
optionsOrGetter: any, | ||
options: ColumnOptions<any, any, any> | ||
): BaseDefinition<TId, TRecord, any> { | ||
const getter = | ||
typeof optionsOrGetter === 'function' | ||
? optionsOrGetter | ||
: typeof optionsOrGetter === 'string' | ||
? (item: TRecord) => item[optionsOrGetter as keyof TRecord] | ||
: (item: TRecord) => item[columnId as unknown as keyof TRecord] | ||
|
||
return { | ||
id: columnId, | ||
label: options.label, | ||
getter, | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
@xen-orchestra/web-core/lib/composables/table/create-define-column.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import type { HideRouteQuery } from '@core/composables/hide-route-query.composable' | ||
import type { SortRouteQuery } from '@core/composables/sort-route-query.composable' | ||
import { createBaseDefinition } from '@core/composables/table/create-base-definition' | ||
import { createSortingDefinition } from '@core/composables/table/create-sorting-definition' | ||
import { createVisibilityDefinition } from '@core/composables/table/create-visibility-definition' | ||
import type { ColumnDefinition, ColumnOptions, DefineColumn } from '@core/composables/table/type' | ||
import { reactive } from 'vue' | ||
|
||
export function createDefineColumn<TRecord>( | ||
hideRouteQuery: HideRouteQuery, | ||
sortRouteQuery: SortRouteQuery | ||
): DefineColumn<TRecord> { | ||
return function defineColumn<TId extends string>( | ||
columnId: TId, | ||
optionsOrGetter: any, | ||
optionsOrNone?: any | ||
): ColumnDefinition<TId, TRecord, any, any, any> { | ||
const options = (optionsOrNone ?? optionsOrGetter) as ColumnOptions<any, any, any> | ||
|
||
return reactive({ | ||
...createBaseDefinition(columnId, optionsOrGetter, options), | ||
...createVisibilityDefinition(columnId, hideRouteQuery, options.isHideable), | ||
...createSortingDefinition(columnId, sortRouteQuery, options.compareFn), | ||
}) | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
@xen-orchestra/web-core/lib/composables/table/create-sorting-definition.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import type { SortRouteQuery } from '@core/composables/sort-route-query.composable' | ||
import type { CompareFn, SortingDefinition } from '@core/composables/table/type' | ||
import { computed } from 'vue' | ||
|
||
export function createSortingDefinition<TCompareReturn extends number | unknown>( | ||
columnId: string, | ||
sortRouteQuery: SortRouteQuery, | ||
compareFn: CompareFn<any, TCompareReturn> | undefined | ||
): SortingDefinition<any, TCompareReturn> { | ||
if (compareFn === undefined) { | ||
return { | ||
isSortable: false, | ||
} as SortingDefinition<any, TCompareReturn> | ||
} | ||
|
||
const isSorted = computed(() => sortRouteQuery.value?.id === columnId) | ||
|
||
const isSortedAsc = computed(() => isSorted.value && sortRouteQuery.value?.direction === 'asc') | ||
|
||
const isSortedDesc = computed(() => isSorted.value && sortRouteQuery.value?.direction === 'desc') | ||
|
||
function sort(direction: 'asc' | 'desc' | false, toggleOffIfSameDirection = false) { | ||
const shouldToggleOff = | ||
direction === false || | ||
(toggleOffIfSameDirection && isSorted.value && sortRouteQuery.value?.direction === direction) | ||
|
||
sortRouteQuery.value = shouldToggleOff ? undefined : { id: columnId, direction } | ||
} | ||
|
||
function sortAsc(toggleOffIfSameDirection = false) { | ||
sort('asc', toggleOffIfSameDirection) | ||
} | ||
|
||
function sortDesc(toggleOffIfSameDirection = false) { | ||
sort('desc', toggleOffIfSameDirection) | ||
} | ||
|
||
return { | ||
isSortable: true, | ||
isSorted, | ||
isSortedAsc, | ||
isSortedDesc, | ||
sort, | ||
sortAsc, | ||
sortDesc, | ||
compareFn, | ||
} as SortingDefinition<any, TCompareReturn> | ||
} |
Oops, something went wrong.