vueless
Version:
Vue Styleless UI Component Library, powered by Tailwind CSS.
1,137 lines (1,065 loc) • 33.1 kB
text/typescript
import { ref } from "vue";
import type { Meta, StoryFn } from "@storybook/vue3-vite";
import {
getArgTypes,
getSlotNames,
getSlotsFragment,
getDocsDescription,
} from "../../utils/storybook";
import { getRandomId } from "../../utils/helper";
import defaultConfig from "../config";
import UTable from "../UTable.vue";
import UButton from "../../ui.button/UButton.vue";
import ULink from "../../ui.button-link/ULink.vue";
import UNumber from "../../ui.text-number/UNumber.vue";
import UBadge from "../../ui.text-badge/UBadge.vue";
import URow from "../../ui.container-row/URow.vue";
import UIcon from "../../ui.image-icon/UIcon.vue";
import ULoader from "../../ui.loader/ULoader.vue";
import UInputSearch from "../../ui.form-input-search/UInputSearch.vue";
import UText from "../../ui.text-block/UText.vue";
import tooltip from "../../v.tooltip/vTooltip";
import type { Row, Props, ColumnObject } from "../types";
import { StickySide } from "../types";
interface UTableArgs extends Props {
slotTemplate?: string;
enum: "size";
numberOfRows: number;
row: Row | typeof getRow;
}
export default {
id: "7010",
title: "Data / Table",
component: UTable,
argTypes: {
...getArgTypes(UTable.__name),
row: {
table: {
disable: true,
},
},
},
args: {
columns: [
{ key: "orderId", label: "Order Id", thClass: "w-2/5" },
{ key: "customerName", label: "Customer Name" },
{ key: "status", label: "Status" },
{ key: "totalPrice", label: "Total Price" },
],
rows: [
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
{
id: "row-3",
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
],
},
parameters: {
docs: {
...getDocsDescription(UTable.__name),
},
},
} as Meta;
function getDateDividerRow(rowAmount: number) {
return Array(rowAmount)
.fill({})
.map((_, index) => {
let rowDate = new Date().toString();
if (index > 1) {
const date = new Date();
date.setFullYear(date.getFullYear());
rowDate = date.toDateString();
}
return {
id: getRandomId(),
rowDate,
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
};
});
}
function getRow(numberOfRows: number) {
return Array.from({ length: numberOfRows }, () => ({
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
}));
}
const DefaultTemplate: StoryFn<UTableArgs> = (args: UTableArgs) => ({
components: { UTable, UButton, ULink, UNumber, UBadge, URow, UIcon, ULoader },
setup: () => ({ args, slots: getSlotNames(UTable.__name) }),
template: `
<UTable v-bind="args">
${args.slotTemplate || getSlotsFragment("")}
</UTable>
`,
});
export const Default = DefaultTemplate.bind({});
Default.args = {};
export const Loading: StoryFn<UTableArgs> = (args: UTableArgs) => ({
components: { UTable, UButton },
setup: () => ({ args }),
template: `
<UButton
label="Toggle loading"
size="sm"
class="mb-4"
@click="args.loading = !args.loading"
/>
<UTable
:columns="[
{ key: 'orderId', label: 'Order Id', thClass: 'w-2/5' },
{ key: 'customerName', label: 'Customer Name' },
{ key: 'status', label: 'Status' },
{ key: 'totalPrice', label: 'Total Price' },
]"
:rows="[]"
:loading="args.loading"
/>
`,
});
Loading.parameters = {
docs: {
description: {
story: "Set table loader state.",
},
},
};
export const EmptyCellLabel = DefaultTemplate.bind({});
EmptyCellLabel.args = {
emptyCellLabel: "NO DATA",
rows: [
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
],
};
EmptyCellLabel.parameters = {
docs: {
description: {
story: "Label to display for empty cell values.",
},
},
};
export const Nesting = DefaultTemplate.bind({});
Nesting.args = {
rows: [
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Processing", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
row: [
{
id: getRandomId(),
orderId: "Suborder-1",
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
row: {
id: getRandomId(),
orderId: "Extra Services",
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
},
{
id: getRandomId(),
orderId: "Suborder-2",
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
row: {
id: getRandomId(),
orderId: "Extra Services",
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
},
],
},
],
};
Nesting.parameters = {
docs: {
description: {
story:
"If you need to have nested row(s) in the table, you can use the `row` key inside a row object.",
},
},
};
export const CellClasses = DefaultTemplate.bind({});
CellClasses.args = {
rows: [
{
id: getRandomId(),
orderId: { value: `ORD-${Math.floor(Math.random() * 10000)}`, class: "bg-error/25" },
customerName: "John Doe",
status: "Cancelled",
totalPrice: "$18.92",
},
{
id: getRandomId(),
class: "!bg-success/25",
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: "Bob Smith",
status: "Delivered",
totalPrice: "$173.11",
},
{
id: getRandomId(),
orderId: {
value: `ORD-${Math.floor(Math.random() * 10000)}`,
contentClass: "text-green-300 line-through",
},
customerName: "Helen Williams",
status: "Delivered",
totalPrice: "$314.26",
},
],
};
CellClasses.parameters = {
docs: {
description: {
story:
// eslint-disable-next-line vue/max-len
"To apply classes to a table content, you may use different approaches: <br/> 1. Pass a string with classes to the `class` key in a row object (classes are applied to the whole row). <br/> 2. Pass a string with classes to the `class` key in a cell object (classes are applied to the table cell, while the value is passed via a `value` key). <br/> 3. Pass a string with classes to the `contentClass` key in a cell object (classes are applied to the cell content, while the value is passed via a `value` key).",
},
},
};
export const Empty = DefaultTemplate.bind({});
Empty.args = { rows: [] };
export const Selectable = DefaultTemplate.bind({});
Selectable.args = { selectable: true };
export const StickyHeader = DefaultTemplate.bind({});
StickyHeader.args = {
rows: getRow(10),
selectable: true,
stickyHeader: true,
};
StickyHeader.parameters = {
docs: {
story: {
inline: false,
iframeHeight: 450,
},
source: {
code: `
<UTable
sticky-header
selectable
:columns="[
{'key':'orderId','label':'Order Id','thClass':'w-2/5'},
{'key':'customerName','label':'Customer Name'},
{'key':'status','label':'Status'},
{'key':'totalPrice','label':'Total Price'}
]"
:rows="[
{
'id': 'xsJCpznyamFLstB',
'orderId': 'ORD-2339',
'customerName': 'James Wilson',
'status': 'Pending',
'totalPrice': '$240.15'
},
{
'id': 'RMHWrRRYAfpmPtR',
'orderId': 'ORD-2927',
'customerName': 'James Wilson',
'status': 'Cancelled',
'totalPrice': '$350.40'
},
{
'id': 'HqCFkWgubiNvVhd',
'orderId': 'ORD-5975',
'customerName': 'Alice Johnson',
'status': 'Shipped',
'totalPrice': '$180.41'
},
{
'id': 'nQyrwynyqIRUTPM',
'orderId': 'ORD-8643',
'customerName': 'Michael Smith',
'status': 'Pending',
'totalPrice': '$318.30'
}
]"
/>
`,
},
},
};
export const StickyFooter = DefaultTemplate.bind({});
StickyFooter.args = {
rows: getRow(10),
selectable: true,
stickyFooter: true,
slotTemplate: `
<template #footer>
<td colspan="4" class="p-4">
<p class="font-semibold text-accented">
📊 Summary: 50 transactions processed | Total Revenue: <strong>$12,345.67</strong>
</p>
</td>
</template>
`,
};
StickyFooter.parameters = {
docs: {
story: {
inline: false,
iframeHeight: 450,
},
source: {
code: `
<UTable
sticky-footer
selectable
:columns="[
{'key':'orderId','label':'Order Id','thClass':'w-2/5'},
{'key':'customerName','label':'Customer Name'},
{'key':'status','label':'Status'},
{'key':'totalPrice','label':'Total Price'}
]"
:rows="[
{
'id': 'xsJCpznyamFLstB',
'orderId': 'ORD-2339',
'customerName': 'James Wilson',
'status': 'Pending',
'totalPrice': '$240.15'
},
{
'id': 'RMHWrRRYAfpmPtR',
'orderId': 'ORD-2927',
'customerName': 'James Wilson',
'status': 'Cancelled',
'totalPrice': '$350.40'
},
{
'id': 'HqCFkWgubiNvVhd',
'orderId': 'ORD-5975',
'customerName': 'Alice Johnson',
'status': 'Shipped',
'totalPrice': '$180.41'
},
{
'id': 'nQyrwynyqIRUTPM',
'orderId': 'ORD-8643',
'customerName': 'Michael Smith',
'status': 'Pending',
'totalPrice': '$318.30'
}
]"
/>
`,
},
},
};
export const Compact = DefaultTemplate.bind({});
Compact.args = { compact: true };
Compact.parameters = {
docs: {
description: {
story: "`compact` prop makes the table compact (fewer spacings).",
},
},
};
export const DateDivider = DefaultTemplate.bind({});
DateDivider.args = { dateDivider: true, rows: getDateDividerRow(4) };
DateDivider.parameters = {
docs: {
description: {
story: "Show date divider line between dates.",
},
},
};
export const DateDividerCustomLabel = DefaultTemplate.bind({});
DateDividerCustomLabel.args = {
rows: getDateDividerRow(4),
dateDivider: [
{
date: getDateDividerRow(4).at(2)!.rowDate,
label: new Date().toLocaleDateString(undefined, {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
}),
config: { label: "!text-orange-400", divider: "!border-orange-300" },
},
],
};
DateDividerCustomLabel.parameters = {
docs: {
description: {
story:
"You can customize date divider by passing necessary data in `date`, `label` and `config` object keys.",
},
},
};
export const StickyColumns: StoryFn<UTableArgs> = (args: UTableArgs) => ({
components: { UTable, URow, UBadge, UButton, UIcon },
directives: { tooltip },
setup() {
function toggleLeft(key: string) {
const column = args.columns.find((item) => (item as ColumnObject).key === key);
if (!column || typeof column === "string") return;
column.sticky = column.sticky === StickySide.Left ? undefined : StickySide.Left;
args.columns = args.columns.slice();
}
function toggleRight(key: string) {
const column = args.columns.find((item) => (item as ColumnObject).key === key);
if (!column || typeof column === "string") return;
column.sticky = column.sticky === StickySide.Right ? undefined : StickySide.Right;
args.columns = args.columns.slice();
}
function isPinned(key: string, side: string) {
const column = args.columns.find((item) => (item as ColumnObject).key === key);
if (!column || typeof column === "string") return;
return column?.sticky === side;
}
return { args, toggleLeft, toggleRight, isPinned };
},
template: `
<UTable v-bind="args">
<template #header-orderId="{ column }">
<URow gap="2xs" align="center">
<div>{{ column.label }}</div>
<UIcon
name="keep"
size="xs"
interactive
v-tooltip="isPinned('orderId', 'left') ? 'Unpin left' : 'Pin left'"
:color="isPinned('orderId', 'left') ? 'primary' : 'inherit'"
@click.stop="toggleLeft('orderId')"
/>
</URow>
</template>
<template #header-customerName="{ column }">
<URow gap="2xs" align="center">
<div>{{ column.label }}</div>
<UIcon
name="keep"
size="xs"
interactive
v-tooltip="isPinned('customerName', 'left') ? 'Unpin left' : 'Pin left'"
:color="isPinned('customerName', 'left') ? 'primary' : 'inherit'"
@click.stop="toggleLeft('customerName')"
/>
</URow>
</template>
<template #header-email="{ column }">
<URow gap="2xs" align="center">
<div>{{ column.label }}</div>
<UIcon
name="keep"
size="xs"
interactive
v-tooltip="isPinned('email', 'left') ? 'Unpin left' : 'Pin left'"
:color="isPinned('email', 'left') ? 'primary' : 'inherit'"
@click.stop="toggleLeft('email')"
/>
</URow>
</template>
<template #header-totalPrice="{ column }">
<URow gap="2xs" align="center">
<div>{{ column.label }}</div>
<UIcon
name="keep"
size="xs"
interactive
v-tooltip="isPinned('totalPrice', 'right') ? 'Unpin right' : 'Pin right'"
:color="isPinned('totalPrice', 'right') ? 'primary' : 'inherit'"
@click.stop="toggleRight('totalPrice')"
/>
</URow>
</template>
<template #header-action="{ column }">
<URow gap="2xs" align="center">
<div>{{ column.label }}</div>
<UIcon
name="keep"
size="xs"
interactive
v-tooltip="isPinned('action', 'right') ? 'Unpin right' : 'Pin right'"
:color="isPinned('action', 'right') ? 'primary' : 'inherit'"
@click.stop="toggleRight('action')"
/>
</URow>
</template>
<template #cell-status="{ value }">
<UBadge
:label="value"
variant="soft"
:color="
value === 'Delivered' ? 'success' :
value === 'Cancelled' ? 'error' :
value === 'Pending' ? 'notice' :
value === 'Shipped' ? 'info' : ''
"
/>
</template>
<template #cell-action>
<UButton label="View" size="xs" variant="soft" />
</template>
</UTable>
`,
});
StickyColumns.args = {
columns: [
{ key: "orderId", label: "Order ID", sticky: "left" },
{ key: "customerName", label: "Customer Name", thClass: "min-w-[200px]" },
{ key: "email", label: "Email", thClass: "min-w-[250px]", sticky: "left" },
{ key: "phone", label: "Phone", thClass: "min-w-[150px]" },
{ key: "address", label: "Address", thClass: "min-w-[300px]" },
{ key: "city", label: "City", thClass: "min-w-[150px]" },
{ key: "country", label: "Country", thClass: "min-w-[150px]" },
{ key: "status", label: "Status", thClass: "min-w-[120px]" },
{ key: "totalPrice", label: "Total Price", thClass: "min-w-[120px]" },
{ key: "action", label: "Actions", sticky: "right", thClass: "min-w-[100px]" },
],
rows: [
{
id: "row-1",
orderId: "ORD-1001",
customerName: "Alice Johnson",
email: "alice.johnson@example.com",
phone: "+1 (555) 123-4567",
address: "123 Main Street, Apt 4B",
city: "New York",
country: "USA",
status: "Delivered",
totalPrice: "$245.99",
action: "View",
},
{
id: "row-2",
orderId: "ORD-1002",
customerName: "Michael Smith",
email: "michael.smith@example.com",
phone: "+1 (555) 234-5678",
address: "456 Oak Avenue, Suite 200",
city: "Los Angeles",
country: "USA",
status: "Pending",
totalPrice: "$189.50",
action: "View",
},
{
id: "row-3",
orderId: "ORD-1003",
customerName: "Emma Brown",
email: "emma.brown@example.com",
phone: "+1 (555) 345-6789",
address: "789 Pine Road, Building C",
city: "Chicago",
country: "USA",
status: "Shipped",
totalPrice: "$312.75",
action: "View",
},
{
id: "row-4",
orderId: "ORD-1004",
customerName: "James Wilson",
email: "james.wilson@example.com",
phone: "+1 (555) 456-7890",
address: "321 Elm Street, Floor 3",
city: "Houston",
country: "USA",
status: "Cancelled",
totalPrice: "$156.20",
action: "View",
},
{
id: "row-5",
orderId: "ORD-1005",
customerName: "Sophia Davis",
email: "sophia.davis@example.com",
phone: "+1 (555) 567-8901",
address: "654 Maple Drive, Unit 12",
city: "Phoenix",
country: "USA",
status: "Delivered",
totalPrice: "$428.90",
action: "View",
},
],
};
StickyColumns.parameters = {
docs: {
description: {
story:
"Pin columns to the left or right edge of the table when scrolling horizontally. " +
"Use the header pin icons to toggle pinning for each column.",
},
},
};
export const HeaderCounterSlot = DefaultTemplate.bind({});
HeaderCounterSlot.args = {
selectable: true,
config: { headerCellCheckbox: "w-20" },
slotTemplate: `
<template #header-counter="{ total }">
Total: {{ total }}
</template>
`,
};
export const HeaderKeySlot = DefaultTemplate.bind({});
HeaderKeySlot.args = {
slotTemplate: `
<template #header-status="{ column }">
<UBadge :label="column.label" />
</template>
`,
};
export const HeaderActionsSlot = DefaultTemplate.bind({});
HeaderActionsSlot.args = {
selectable: true,
selectedRows: [{ id: "row-3" }],
slotTemplate: `
<template #header-actions>
<URow gap="2xs">
<UButton
label="Edit"
variant="ghost"
color="primary"
size="sm"
/>
<UButton
label="Delete"
variant="ghost"
color="primary"
size="sm"
/>
</URow>
</template>
`,
};
export const BeforeHeaderSlot = DefaultTemplate.bind({});
BeforeHeaderSlot.args = {
slotTemplate: `
<template #before-header="{ colsCount, classes }">
<th :colspan="colsCount" :class="classes">📊 Latest orders report.</th>
</template>
`,
};
export const BeforeFirstRowSlot = DefaultTemplate.bind({});
BeforeFirstRowSlot.args = {
slotTemplate: `
<template #before-first-row>
<UButton label="Load planned" size="xs" class="my-3" />
</template>
`,
};
export const CellSlots = DefaultTemplate.bind({});
CellSlots.args = {
slotTemplate: `
<template #cell-orderId="{ value }">
<ULink :label="value" color="success" />
</template>
<template #cell-status="{ value }">
<UBadge
:label="value"
variant="soft"
:color="
value === 'Delivered' ? 'success' :
value === 'Cancelled' ? 'error' :
value === 'Pending' ? 'notice' :
value === 'Shipped' ? 'info' : ''
"
/>
</template>
<template #cell-totalPrice="{ value }">
<UNumber :value="value.slice(1)" currency="€" currency-align="left" />
</template>
`,
};
export const ExpandSlot = DefaultTemplate.bind({});
ExpandSlot.args = {
rows: [
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Processing", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
row: [
{
id: getRandomId(),
orderId: "Suborder-1",
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
row: {
id: getRandomId(),
orderId: "Extra Services",
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
},
{
id: getRandomId(),
orderId: "Suborder-2",
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
row: {
id: getRandomId(),
orderId: "Extra Services",
customerName: "",
status: "",
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
},
],
},
],
slotTemplate: `
<template #expand="{ expanded }">
<UButton
:icon="expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down'"
variant="soft"
size="2xs"
square
/>
</template>
`,
};
export const NestedRowSlot = DefaultTemplate.bind({});
NestedRowSlot.args = {
columns: [
{ key: "orderId", label: "Order Id" },
{ key: "customerName", label: "Customer Name" },
{ key: "status", label: "Status" },
],
rows: [
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Processing", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: "John Doe",
status: "Processing",
row: {
id: getRandomId(),
rows: [
{
id: getRandomId(),
category: "Gadgets",
itemName: "Ergonomic Mouse",
quantity: 2,
},
{
id: getRandomId(),
category: "Gadgets",
itemName: "Wireless Keyboard",
quantity: 1,
},
{
id: getRandomId(),
category: "Electronics",
itemName: "USB-C Hub",
quantity: 3,
},
],
},
},
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Processing", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
{
id: getRandomId(),
orderId: `ORD-${Math.floor(Math.random() * 10000)}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][
Math.floor(Math.random() * 4)
],
status: ["Processing", "Shipped", "Delivered", "Cancelled"][Math.floor(Math.random() * 4)],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
},
],
slotTemplate: `
<template #nested-row="{ row }">
<div class="p-4 bg-lifted">
<UTable
:columns="[
{ key: 'category', label: 'Category' },
{ key: 'itemName', label: 'Product' },
{ key: 'quantity', label: 'Quantity' },
]"
:rows="row.rows"
compact
/>
</div>
</template>
`,
};
export const AfterLastRowSlot = DefaultTemplate.bind({});
AfterLastRowSlot.args = {
slotTemplate: `
<template #after-last-row="{ colsCount, classes }">
<td :colspan="colsCount" :class="classes">
<URow block justify="end">
Totals:
<UNumber
color="success"
:value="${Math.random() * 500}"
currency="$"
/>
</URow>
</td>
</template>
`,
};
export const EmptyStateSlot = DefaultTemplate.bind({});
EmptyStateSlot.args = {
rows: [],
config: {
i18n: { ...defaultConfig.i18n, noData: "Fetching data..." },
bodyEmptyStateCell: "py-10",
},
slotTemplate: `
<template #empty-state>
<ULoader loading :config="{ loader: 'mx-auto mb-4' }" />
<p class="text-center">Fetching latest data, please wait...</p>
</template>
`,
};
export const FooterSlot = DefaultTemplate.bind({});
FooterSlot.args = {
slotTemplate: `
<template #footer>
<td colspan="100%" class="px-2">
🔍 For more detailed insights, please visit our data analysis page or reach out to support for assistance.
</td>
</template>
`,
};
function generateNestedRows(count: number): Row[] {
return Array.from({ length: count }, (_, i) => {
const hasChildren = i % 10 === 0;
const row: Row = {
id: `row-${i}`,
orderId: `ORD-${String(i).padStart(5, "0")}`,
customerName: ["Alice Johnson", "Michael Smith", "Emma Brown", "James Wilson"][i % 4],
status: ["Pending", "Shipped", "Delivered", "Cancelled"][i % 4],
totalPrice: `$${(Math.random() * 500).toFixed(2)}`,
};
if (hasChildren) {
row.row = Array.from({ length: 3 }, (_, j) => ({
id: `row-${i}-child-${j}`,
orderId: `SUB-${String(i).padStart(5, "0")}-${j + 1}`,
customerName: "",
status: ["Processing", "Packed", "Ready"][j % 3],
totalPrice: `$${(Math.random() * 100).toFixed(2)}`,
}));
}
return row;
});
}
export const VirtualScroll: StoryFn<UTableArgs> = (args: UTableArgs) => ({
components: { UTable, UBadge },
setup() {
const rows = generateNestedRows(100000);
return { args, rows };
},
template: `
<UTable
:columns="[
{ key: 'orderId', label: 'Order ID', thClass: 'w-1/5' },
{ key: 'customerName', label: 'Customer Name' },
{ key: 'status', label: 'Status' },
{ key: 'totalPrice', label: 'Total Price' },
]"
:rows="rows"
compact
virtual-scroll
:row-height="46"
:buffer-size="10"
>
<template #cell-status="{ value }">
<UBadge
:label="value"
variant="soft"
:color="
value === 'Delivered' ? 'success' :
value === 'Cancelled' ? 'error' :
value === 'Pending' ? 'notice' :
value === 'Shipped' || value === 'Processing' ? 'info' :
value === 'Packed' || value === 'Ready' ? 'primary' : ''
"
/>
</template>
</UTable>
`,
});
VirtualScroll.parameters = {
docs: {
description: {
story:
"Virtual scrolling enables rendering of large datasets (100,000+ rows) with optimal performance. " +
"Only visible rows are rendered in the DOM, with collapsible nested rows fully supported. " +
"Use `virtualScroll` prop to enable, and configure `rowHeight`, `scrollHeight`, and `bufferSize` as needed.",
},
},
};
export const VirtualSearch: StoryFn<UTableArgs> = (args: UTableArgs) => ({
components: { UTable, UInputSearch, UButton, URow, UText },
setup() {
const rows = generateNestedRows(100000);
const search = ref("");
const searchMatch = ref(-1);
const totalMatches = ref(0);
function onPrev() {
if (totalMatches.value === 0) return;
searchMatch.value = searchMatch.value <= 0 ? totalMatches.value - 1 : searchMatch.value - 1;
}
function onNext() {
if (totalMatches.value === 0) return;
searchMatch.value = searchMatch.value >= totalMatches.value - 1 ? 0 : searchMatch.value + 1;
}
return { args, rows, search, searchMatch, totalMatches, onPrev, onNext };
},
template: `
<URow align="stretch" gap="xs" class="mb-4">
<UInputSearch
v-model="search"
placeholder="Search in table..."
size="md"
@clear="searchMatch = -1"
>
<template #right>
<UText :label="searchMatch + 1 + ' / ' + totalMatches" :wrap="false" class="ml-1" />
</template>
</UInputSearch>
<UButton
square
size="sm"
title="Prev"
variant="soft"
icon="keyboard_arrow_up"
:disabled="totalMatches === 0"
@click="onPrev"
/>
<UButton
square
size="sm"
title="Next"
variant="soft"
icon="keyboard_arrow_down"
:disabled="totalMatches === 0"
@click="onNext"
/>
</URow>
<UTable
:columns="[
{ key: 'orderId', label: 'Order ID', thClass: 'w-1/5' },
{ key: 'customerName', label: 'Customer Name' },
{ key: 'status', label: 'Status' },
{ key: 'totalPrice', label: 'Total Price' },
]"
:rows="rows"
compact
virtual-scroll
:row-height="45"
:buffer-size="10"
:search="search"
:search-match="searchMatch"
@search="totalMatches = $event"
/>
`,
});
VirtualSearch.parameters = {
docs: {
description: {
story:
"Search functionality with virtual scrolling. " +
"Use `search` prop to pass a search string and `searchMatch` prop to highlight a specific match. " +
"The `@search` event emits the total number of matches found. " +
"Use Prev/Next buttons to navigate between matches.",
},
},
};