feat: add Table component (#237)

This commit is contained in:
Benjamin Canac
2023-05-30 12:13:57 +02:00
committed by GitHub
parent 4a99d6a7bb
commit cce000ab2b
24 changed files with 1087 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="[&>div>pre]:!rounded-t-none">
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass]">
<div class="flex border border-gray-200 dark:border-gray-700 relative not-prose rounded-t-md overflow-x-auto" :class="[{ 'p-4': padding, 'rounded-b-md': !$slots.code, 'border-b-0': !!$slots.code }, backgroundClass]">
<ContentSlot v-if="$slots.default" :use="$slots.default" />
</div>

View File

@@ -0,0 +1,43 @@
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :rows="people" />
</template>

View File

@@ -0,0 +1,59 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'User name'
}, {
key: 'title',
label: 'Job position'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :columns="columns" :rows="people" />
</template>

View File

@@ -0,0 +1,68 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const selectedColumns = ref([...columns])
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<div>
<div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
<USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
</div>
<UTable :columns="selectedColumns" :rows="people" />
</div>
</template>

View File

@@ -0,0 +1,63 @@
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'title',
label: 'Title',
sortable: true
}, {
key: 'email',
label: 'Email',
sortable: true
}, {
key: 'role',
label: 'Role'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :columns="columns" :rows="people" />
</template>

View File

@@ -0,0 +1,63 @@
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
const q = ref('')
const filteredRows = computed(() => {
if (!q.value) {
return people
}
return people.filter((person) => {
return Object.values(person).some((value) => {
return String(value).toLowerCase().includes(q.value.toLowerCase())
})
})
})
</script>
<template>
<div>
<div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
<UInput v-model="q" placeholder="Filter people..." />
</div>
<UTable :rows="filteredRows" />
</div>
</template>

View File

@@ -0,0 +1,45 @@
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" />
</template>

View File

@@ -0,0 +1,91 @@
<script setup>
const columns = [{
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}, {
key: 'actions'
}]
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
const items = (row) => [
[{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
click: () => console.log('Edit', row.id)
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid'
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}], [{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid'
}]
]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" :columns="columns">
<template #name-data="{ row }">
<span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
</template>
<template #actions-data="{ row }">
<UDropdown :items="items(row)">
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
</UDropdown>
</template>
</UTable>
</template>

View File

@@ -0,0 +1,437 @@
---
github: true
description: 'Display data in a table.'
---
## Usage
Use the `rows` prop to set the data to display in the table. By default, the table will display all the fields of the rows.
::component-example
---
padding: false
---
#default
:table-example-basic{class="w-full"}
#code
```vue
<script setup>
const people = [{
id: 1,
name: 'Lindsay Walton',
title: 'Front-end Developer',
email: 'lindsay.walton@example.com',
role: 'Member'
}, {
id: 2,
name: 'Courtney Henry',
title: 'Designer',
email: 'courtney.henry@example.com',
role: 'Admin'
}, {
id: 3,
name: 'Tom Cook',
title: 'Director of Product',
email: 'tom.cook@example.com',
role: 'Member'
}, {
id: 4,
name: 'Whitney Francis',
title: 'Copywriter',
email: 'whitney.francis@example.com',
role: 'Admin'
}, {
id: 5,
name: 'Leonard Krasner',
title: 'Senior Designer',
email: 'leonard.krasner@example.com',
role: 'Owner'
}, {
id: 6,
name: 'Floyd Miles',
title: 'Principal Designer',
email: 'floyd.miles@example.com',
role: 'Member'
}]
</script>
<template>
<UTable :rows="people" />
</template>
```
::
### Columns
Use the `columns` prop to configure which columns to display. It's an array of objects with the following properties:
- `label` - The label to display in the table header. Can be changed through the `column-attribute` prop.
- `key` - The field to display from the row data.
- `sortable` - Whether the column is sortable. Defaults to `false`.
::component-example
---
padding: false
---
#default
:table-example-columns{class="w-full"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'User name'
}, {
key: 'title',
label: 'Job position'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role'
}]
const people = [...]
</script>
<template>
<UTable :columns="columns" :rows="people" />
</template>
```
::
You can easily use the [SelectMenu](/forms/select-menu) component to change the columns to display.
::component-example
---
padding: false
---
#default
:table-example-columns-selectable{class="w-full"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name'
}, {
key: 'title',
label: 'Title'
}, {
key: 'email',
label: 'Email'
}, {
key: 'role',
label: 'Role'
}]
const selectedColumns = ref([...columns])
const people = [...]
</script>
<template>
<div>
<USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
<UTable :columns="selectedColumns" :rows="people" />
</div>
</template>
```
::
### Sortable
You can make the columns sortable by setting the `sortable` property to `true` in the column configuration.
::component-example
---
padding: false
---
#default
:table-example-columns-sortable{class="w-full"}
#code
```vue
<script setup>
const columns = [{
key: 'id',
label: 'ID'
}, {
key: 'name',
label: 'Name',
sortable: true
}, {
key: 'title',
label: 'Title',
sortable: true
}, {
key: 'email',
label: 'Email',
sortable: true
}, {
key: 'role',
label: 'Role'
}]
const people = [...]
</script>
<template>
<UTable :columns="columns" :rows="people" />
</template>
```
::
Use the `sort-button` prop to customize the sort button in the header. You can pass all the props of the [Button](/elements/button) component to customize it through this prop or globally through `ui.table.default.sortButton`. Its icon defaults to `i-heroicons-arrows-up-down-20-solid`.
::component-card
---
padding: false
baseProps:
class: 'w-full'
columns:
- key: 'id'
label: 'ID'
- key: 'name'
label: 'Name'
sortable: true
- key: 'title'
label: 'Title'
sortable: true
- key: 'email'
label: 'Email'
sortable: true
- key: 'role'
label: 'Role'
rows:
- id: 1
name: 'Lindsay Walton'
title: 'Front-end Developer'
email: 'lindsay.walton@example.com'
role: 'Member'
- id: 2
name: 'Courtney Henry'
title: 'Designer'
email: 'courtney.henry@example.com'
role: 'Admin'
- id: 3
name: 'Tom Cook'
title: 'Director of Product'
email: 'tom.cook@example.com'
role: 'Member'
- id: 4
name: 'Whitney Francis'
title: 'Copywriter'
email: 'whitney.francis@example.com'
role: 'Admin'
- id: 5
name: 'Leonard Krasner'
title: 'Senior Designer'
email: 'leonard.krasner@example.com'
role: 'Owner'
- id: 6
name: 'Floyd Miles'
title: 'Principal Designer'
email: 'floyd.miles@example.com'
role: 'Member'
props:
sortAscIcon: 'i-heroicons-arrow-up-20-solid'
sortDescIcon: 'i-heroicons-arrow-down-20-solid'
sortButton:
icon: 'i-heroicons-sparkles-20-solid'
color: 'primary'
variant: 'outline'
size: '2xs'
ui:
rounded: 'rounded-full'
excludedProps:
- sortButton
- sortAscIcon
- sortDescIcon
---
::
Use the `sort-asc-icon` prop to set a different icon or change it globally in `ui.table.default.sortAscIcon`. Defaults to `i-heroicons-bars-arrow-up-20-solid`.
Use the `sort-desc-icon` prop to set a different icon or change it globally in `ui.table.default.sortDescIcon`. Defaults to `i-heroicons-bars-arrow-down-20-solid`.
::alert{icon="i-heroicons-light-bulb"}
You can also customize the entire header cell, read more in the [Slots](#slots) section.
::
### Selectable
Use a `v-model` to make the table selectable. The `v-model` will be an array of the selected rows.
::component-example
---
padding: false
---
#default
:table-example-selectable{class="w-full"}
#code
```vue
<script setup>
const people = [...]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" />
</template>
```
::
::alert{icon="i-heroicons-light-bulb"}
You can use the `by` prop to compare objects by a field instead of comparing object instances. We've replicated the behavior of Headless UI [Combobox](https://headlessui.com/vue/combobox#binding-objects-as-values).
::
### Searchable
You can easily use the [Input](/forms/input) component to filter the rows.
::component-example
---
padding: false
---
#default
:table-example-searchable{class="w-full"}
#code
```vue
<script setup>
const people = [...]
const q = ref('')
const filteredRows = computed(() => {
if (!q.value) {
return people
}
return people.filter((person) => {
return Object.values(person).some((value) => {
return String(value).toLowerCase().includes(q.value.toLowerCase())
})
})
})
</script>
<template>
<div>
<UInput v-model="q" placeholder="Filter people..." />
<UTable :rows="filteredRows" />
</div>
</template>
```
::
### Slots
You can use slots to customize the header and data cells of the table.
#### Header
Use the `#<column>-header` slot to customize the header cell of a column. You will have access to the `column`, `sort` and `on-sort` properties in the slot scope.
The `sort` property is an object with the following properties:
- `field` - The field to sort by.
- `direction` - The direction to sort by. Can be `asc` or `desc`.
The `on-sort` property is a function that you can call to sort the table. It accepts a string with the field to sort by and an optional string with the direction to sort by. If the direction is not provided, it will toggle between `asc` and `desc`.
::alert{icon="i-heroicons-light-bulb"}
Even though you can customize the sort button as mentioned in the [Sortable](#sortable) section, you can use this slot to completely override its behavior, with a custom dropdown for example.
::
#### Data
Use the `#<column>-data` slot to customize the data cell of a column. You will have access to the `row` and `column` properties in the slot scope.
#### Example
You can for example create an extra column for actions with a [Dropdown](/elements/dropdown) component inside or change the color of the rows based on a selection.
::component-example
---
padding: false
---
#default
:table-example-slots{class="w-full"}
#code
```vue
<script setup>
const columns = [..., {
key: 'actions'
}]
const people = [...]
const items = (row) => [
[{
label: 'Edit',
icon: 'i-heroicons-pencil-square-20-solid',
click: () => console.log('Edit', row.id)
}, {
label: 'Duplicate',
icon: 'i-heroicons-document-duplicate-20-solid'
}], [{
label: 'Archive',
icon: 'i-heroicons-archive-box-20-solid'
}, {
label: 'Move',
icon: 'i-heroicons-arrow-right-circle-20-solid'
}], [{
label: 'Delete',
icon: 'i-heroicons-trash-20-solid'
}]
]
const selected = ref([people[1]])
</script>
<template>
<UTable v-model="selected" :rows="people" :columns="columns">
<template #name-data="{ row }">
<span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
</template>
<template #actions-data="{ row }">
<UDropdown :items="items(row)">
<UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
</UDropdown>
</template>
</UTable>
</template>
```
::
## Props
:component-props
## Preset
:component-preset

View File

@@ -257,7 +257,7 @@ excludedProps:
The CommandPalette component takes care of the full-text search for you with [Fuse.js](https://fusejs.io). You can pass all the options of Fuse.js through the `fuse` prop.
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behaviour by overriding the `command-attribute` prop. This will also affect the display of the command.
When searching for a command, the component will look for a `label` property on the command by default. You can customize this behavior by overriding the `command-attribute` prop. This will also affect the display of the command.
You can also highlight the matches in the command by setting the `fuse.fuseOptions.includeMatches` to `true`. The CommandPalette component automatically takes care of the highlighting for you.
@@ -320,7 +320,7 @@ const groups = computed(() => {
::
::alert{icon="i-heroicons-light-bulb"}
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behaviour by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
The `loading` state will automatically be enabled when a `search` function is loading. You can disable this behavior by setting the `loading-icon` prop to `null` or globally in `ui.commandPalette.default.loadingIcon`.
::
## Themes

View File

@@ -36,7 +36,7 @@ const toast = useToast()
```
::
This component will render by default the notifications at the bottom right of the screen. You can configure its behaviour in the `app.config.ts` through `ui.notifications`:
This component will render by default the notifications at the bottom right of the screen. You can configure its behavior in the `app.config.ts` through `ui.notifications`:
```ts [app.config.ts]
export default defineAppConfig({

View File

@@ -203,6 +203,12 @@ export default defineNuxtModule<ModuleOptions>({
global: options.global,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'data'),
prefix: options.prefix,
global: options.global,
watch: false
})
addComponentsDir({
path: resolve(runtimeDir, 'components', 'layout'),
prefix: options.prefix,

View File

@@ -1,3 +1,30 @@
// Data
const table = {
wrapper: 'relative',
container: 'min-w-full table-fixed divide-y divide-gray-300 dark:divide-gray-700',
thead: '',
tbody: 'divide-y divide-gray-200 dark:divide-gray-800',
tr: {
base: '',
selected: 'bg-gray-50 dark:bg-gray-800/50'
},
th: 'px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white',
td: 'whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400',
default: {
sortAscIcon: 'i-heroicons-bars-arrow-up-20-solid',
sortDescIcon: 'i-heroicons-bars-arrow-down-20-solid',
sortButton: {
icon: 'i-heroicons-arrows-up-down-20-solid',
trailing: true,
square: true,
color: 'gray',
variant: 'ghost',
class: '-m-1.5'
}
}
}
// Elements
const avatar = {
@@ -411,7 +438,7 @@ const selectMenu = {
const radio = {
wrapper: 'relative flex items-start',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
base: 'h-4 w-4 text-primary-500 dark:text-primary-400 focus-visible:ring-2 focus-visible:ring-offset-2 bg-white dark:bg-gray-900 dark:checked:bg-current dark:checked:border-transparent dark:indeterminate:bg-current dark:indeterminate:border-transparent focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900 border-gray-300 dark:border-gray-700 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-0 focus:ring-transparent focus:ring-offset-transparent',
label: 'font-medium text-gray-700 dark:text-gray-200',
required: 'text-red-500 dark:text-red-400',
help: 'text-gray-500 dark:text-gray-400'
@@ -766,6 +793,7 @@ const notifications = {
export default {
ui: {
table,
avatar,
avatarGroup,
badge,

View File

@@ -0,0 +1,168 @@
<template>
<div :class="ui.wrapper">
<table :class="ui.container">
<thead :class="ui.thead">
<tr :class="ui.tr.base">
<th v-if="modelValue" scope="col" class="pl-4">
<UCheckbox :checked="indeterminate || selected.length === rows.length" :indeterminate="indeterminate" @change="selected = $event.target.checked ? rows : []" />
</th>
<th v-for="(column, index) in columns" :key="index" scope="col" :class="ui.th">
<slot :name="`${column.key}-header`" :column="column" :sort="sort" :on-sort="onSort">
<UButton
v-if="column.sortable"
v-bind="sortButton"
:icon="(!sort.field || sort.field !== column.key) ? sortButton.icon : sort.direction === 'asc' ? sortAscIcon : sortDescIcon"
:label="column[columnAttribute]"
@click="onSort(column.key)"
/>
<span v-else>{{ column[columnAttribute] }}</span>
</slot>
</th>
</tr>
</thead>
<tbody :class="ui.tbody">
<tr v-for="(row, index) in rows" :key="index" :class="[ui.tr.base, isSelected(row) && ui.tr.selected]">
<td v-if="modelValue" class="pl-4">
<UCheckbox v-model="selected" :value="row" />
</td>
<td v-for="(column, subIndex) in columns" :key="subIndex" :class="ui.td">
<slot :name="`${column.key}-data`" :column="column" :row="row">
{{ row[column.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, toRaw } from 'vue'
import type { PropType } from 'vue'
import { capitalize, orderBy } from 'lodash-es'
import { defu } from 'defu'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
// @ts-expect-error
import appConfig from '#build/app.config'
// const appConfig = useAppConfig()
function defaultComparator<T>(a: T, z: T): boolean {
return a === z
}
export default defineComponent({
props: {
modelValue: {
type: Array,
default: null
},
by: {
type: [String, Function],
default: () => defaultComparator
},
rows: {
type: Array as PropType<{ [key: string]: any }[]>,
default: () => []
},
columns: {
type: Array as PropType<{ key: string, sortable?: boolean, [key: string]: any }[]>,
default: null
},
columnAttribute: {
type: String,
default: 'label'
},
sortButton: {
type: Object as PropType<Partial<Button>>,
default: () => appConfig.ui.table.default.sortButton
},
sortAscIcon: {
type: String,
default: () => appConfig.ui.table.default.sortAscIcon
},
sortDescIcon: {
type: String,
default: () => appConfig.ui.table.default.sortDescIcon
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.table>>,
default: () => appConfig.ui.table
}
},
emits: ['update:modelValue'],
setup (props, { emit }) {
// TODO: Remove
const appConfig = useAppConfig()
const ui = computed<Partial<typeof appConfig.ui.table>>(() => defu({}, props.ui, appConfig.ui.table))
const sort = ref({ field: null, direction: null })
const columns = computed(() => props.columns ?? Object.keys(props.rows[0] ?? {}).map((key) => ({ key, label: capitalize(key), sortable: false })))
const rows = computed(() => {
if (!sort.value?.field) {
return props.rows
}
const { field, direction } = sort.value
return orderBy(props.rows, field, direction)
})
const selected = computed({
get () {
return props.modelValue
},
set (value) {
emit('update:modelValue', value)
}
})
const indeterminate = computed(() => selected.value && selected.value.length > 0 && selected.value.length < props.rows.length)
function compare (a: any, z: any) {
if (typeof props.by === 'string') {
const property = props.by as unknown as any
return a?.[property] === z?.[property]
}
return props.by(a, z)
}
function isSelected (row) {
if (!props.modelValue) {
return false
}
return selected.value.some((item) => compare(toRaw(item), toRaw(row)))
}
function onSort (field: string, direction?: 'asc' | 'desc') {
if (sort.value.field === field) {
sort.value.direction = direction || sort.value.direction === 'asc' ? 'desc' : 'asc'
} else {
sort.value = { field, direction: direction || 'asc' }
}
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
sort,
// eslint-disable-next-line vue/no-dupe-keys
columns,
// eslint-disable-next-line vue/no-dupe-keys
rows,
selected,
indeterminate,
isSelected,
onSort
}
}
})
</script>

View File

@@ -8,6 +8,8 @@
:required="required"
:value="value"
:disabled="disabled"
:checked="checked"
:indeterminate="indeterminate"
type="checkbox"
:class="[ui.base, ui.custom]"
@focus="$emit('focus', $event)"
@@ -40,7 +42,7 @@ import appConfig from '#build/app.config'
export default defineComponent({
props: {
value: {
type: [String, Number, Boolean],
type: [String, Number, Boolean, Object],
default: null
},
modelValue: {
@@ -55,6 +57,14 @@ export default defineComponent({
type: Boolean,
default: false
},
checked: {
type: Boolean,
default: false
},
indeterminate: {
type: Boolean,
default: false
},
help: {
type: String,
default: null