UNPKG

@shopware-ag/meteor-component-library

Version:

The meteor component library is a Vue component library developed by Shopware. It is based on the [Meteor Design System](https://shopware.design/).

561 lines (465 loc) 16.2 kB
import { Canvas, Meta, Markdown } from "@storybook/blocks"; import * as DataTableStories from "./mt-data-table.stories"; import * as DataTableInteractiveStories from "./mt-data-table.interactive.stories"; <Meta of={DataTableStories} /> # mt-data-table The `mt-data-table` is a powerful and flexible data table component designed to handle large datasets with features like pagination, sorting, filtering, and more. It's implemented as a "dumb" component, which means it focuses solely on rendering and user interactions, while delegating all data management and business logic to the parent component. ## Understanding the Dumb Component Pattern As a dumb component, `mt-data-table` follows these principles: 1. **No Internal State Management**: The component doesn't maintain its own data state. All data must be provided through props and updated by the parent component. 2. **Event-Based Communication**: When user interactions occur (sorting, filtering, pagination, etc.), the component emits events. The parent component must listen to these events and handle the data updates accordingly. 3. **Prop-Driven Updates**: Any changes to the data or UI state must be driven by prop changes from the parent component. This means that as a developer implementing this component, you need to: 1. **Handle Data Loading**: Implement the logic to fetch and manage data, including: - Initial data loading - Pagination - Sorting - Filtering - Search functionality 2. **Manage State**: Maintain the state of: - Current page - Items per page - Sort column and direction - Applied filters - Search term - Selected rows - Loading states 3. **Respond to Events**: Listen to and handle all relevant events to update your data and state. Here's a basic example of implementing the component with proper data management: ```html <template> <mt-data-table :data-source="dataSource" :columns="columns" :pagination-limit="limit" :pagination-current-page="currentPage" :pagination-total-items="totalItems" :is-loading="isLoading" :applied-filters="appliedFilters" @pagination-limit-change="handleLimitChange" @pagination-current-page-change="handlePageChange" @sort-change="handleSortChange" @search-value-change="handleSearchChange" @update:appliedFilters="handleFilterChange" /> </template> <script setup> import { ref, onMounted } from "vue"; // State management const dataSource = ref([]); const limit = ref(10); const currentPage = ref(1); const totalItems = ref(0); const isLoading = ref(false); const appliedFilters = ref([]); const sortBy = ref({ property: "", direction: "" }); const searchTerm = ref(""); // Data fetching const fetchData = async () => { isLoading.value = true; try { const response = await api.fetchData({ page: currentPage.value, limit: limit.value, sort: sortBy.value, filters: appliedFilters.value, search: searchTerm.value, }); dataSource.value = response.data; totalItems.value = response.total; } finally { isLoading.value = false; } }; // Event handlers const handleLimitChange = (newLimit) => { limit.value = newLimit; currentPage.value = 1; // Reset to first page fetchData(); }; const handlePageChange = (newPage) => { currentPage.value = newPage; fetchData(); }; const handleSortChange = ({ property, direction }) => { sortBy.value = { property, direction }; fetchData(); }; const handleSearchChange = (term) => { searchTerm.value = term; currentPage.value = 1; // Reset to first page fetchData(); }; const handleFilterChange = (newFilters) => { appliedFilters.value = newFilters; currentPage.value = 1; // Reset to first page fetchData(); }; // Initial data load onMounted(() => { fetchData(); }); </script> ``` This implementation pattern ensures: - Clear separation of concerns - Predictable data flow - Flexibility in data source and business logic - Reusability across different data scenarios ## Creating a Wrapper Component Since the data management logic can be repetitive across different tables in your application, it often makes sense to create a wrapper component that encapsulates this logic. This approach can: - Reduce boilerplate code - Ensure consistent data handling - Centralize API integration - Make table implementations more maintainable Here's an example of creating a reusable wrapper component: ```html <!-- DataTableWrapper.vue --> <template> <mt-data-table v-bind="$props" :data-source="dataSource" :pagination-limit="limit" :pagination-current-page="currentPage" :pagination-total-items="totalItems" :is-loading="isLoading" :applied-filters="appliedFilters" v-on="$listeners" @pagination-limit-change="handleLimitChange" @pagination-current-page-change="handlePageChange" @sort-change="handleSortChange" @search-value-change="handleSearchChange" @update:appliedFilters="handleFilterChange" > <template v-for="(_, slot) in $slots" #[slot]> <slot :name="slot" /> </template> </mt-data-table> </template> <script setup> import { ref, onMounted } from "vue"; const props = defineProps({ // Forward all mt-data-table props fetchDataFn: { type: Function, required: true, }, defaultSort: { type: Object, default: () => ({ property: "", direction: "" }), }, // ... other props }); // State management const dataSource = ref([]); const limit = ref(10); const currentPage = ref(1); const totalItems = ref(0); const isLoading = ref(false); const appliedFilters = ref([]); const sortBy = ref(props.defaultSort); const searchTerm = ref(""); // Data fetching const fetchData = async () => { isLoading.value = true; try { const response = await props.fetchDataFn({ page: currentPage.value, limit: limit.value, sort: sortBy.value, filters: appliedFilters.value, search: searchTerm.value, }); dataSource.value = response.data; totalItems.value = response.total; } finally { isLoading.value = false; } }; // Event handlers const handleLimitChange = (newLimit) => { limit.value = newLimit; currentPage.value = 1; // Reset to first page fetchData(); }; const handlePageChange = (newPage) => { currentPage.value = newPage; fetchData(); }; const handleSortChange = ({ property, direction }) => { sortBy.value = { property, direction }; fetchData(); }; const handleSearchChange = (term) => { searchTerm.value = term; currentPage.value = 1; // Reset to first page fetchData(); }; const handleFilterChange = (newFilters) => { appliedFilters.value = newFilters; currentPage.value = 1; // Reset to first page fetchData(); }; onMounted(() => { fetchData(); }); </script> ``` Now you can use this wrapper component with much less boilerplate: ```html <!-- YourPage.vue --> <template> <data-table-wrapper :columns="columns" :fetch-data-fn="fetchUsers" :default-sort="{ property: 'name', direction: 'asc' }" /> </template> <script setup> import { ref } from "vue"; import DataTableWrapper from "./DataTableWrapper.vue"; const columns = [ { label: "Name", property: "name", sortable: true, position: 100, }, // ... other columns ]; const fetchUsers = async (params) => { const response = await api.users.list(params); return { data: response.users, total: response.total, }; }; </script> ``` This wrapper approach is particularly useful when you: - Have multiple tables in your application - Need consistent data handling across tables - Want to share common functionality (error handling, loading states, etc.) - Need to maintain the same API integration pattern You can extend the wrapper component to include additional features like: - Error handling and retry logic - Data caching - Custom filtering logic - Export functionality - Bulk action handling - Persistent table state ## Key Features - **Data Rendering**: Displays data in a tabular format with customizable columns - **Pagination**: Built-in pagination support with customizable page sizes - **Sorting**: Column-based sorting with customizable sort directions - **Filtering**: Flexible filtering system with various filter types - **Search**: Global search functionality (can be disabled) - **Row Selection**: Single and multiple row selection with bulk actions - **Column Management**: Drag-and-drop column reordering and resizing - **Loading States**: Built-in loading state handling with skeletons - **Empty States**: Customizable empty state display - **Responsive**: Adapts to different screen sizes with horizontal/vertical scrolling ## Basic Usage Here's a simple example of how to use the `mt-data-table` component: ```html <template> <mt-data-table :data-source="dataSource" :columns="columns" :pagination-limit="10" :pagination-current-page="1" :pagination-total-items="100" :is-loading="isLoading" @pagination-limit-change="handleLimitChange" @pagination-current-page-change="handlePageChange" /> </template> <script setup> import { ref } from "vue"; const dataSource = ref([ { id: "1", name: "John Doe", email: "john@example.com" }, { id: "2", name: "Jane Smith", email: "jane@example.com" }, // ... more data ]); const columns = [ { label: "Name", property: "name", sortable: true, position: 100, }, { label: "Email", property: "email", sortable: true, position: 200, }, ]; const isLoading = ref(false); const handleLimitChange = (limit) => { // Handle page size change }; const handlePageChange = (page) => { // Handle page change }; </script> ``` <Canvas of={DataTableInteractiveStories.VisualTestRenderTable} /> ## Column Configuration Columns are defined through a configuration object that specifies how each column should be rendered and behave: ```typescript interface ColumnDefinition { label: string; // Column header label property: string; // Property path in data source object renderer?: string; // How to render the cell ('text'|'number'|'price') position: number; // Column order (use increments of 100) sortable?: boolean; // Enable/disable sorting (default: true) width?: number; // Fixed column width allowResize?: boolean; // Allow column resizing visible?: boolean; // Show/hide column } ``` ## Props <Markdown> {` | Prop Name | Type | Default | Description | |-----------|------|---------|-------------| | dataSource | Array | [] | Array of objects containing the data to display | | columns | Array | [] | Array of column definitions | | paginationLimit | Number | 10 | Number of items per page | | paginationCurrentPage | Number | 1 | Current active page | | paginationTotalItems | Number | 0 | Total number of items | | isLoading | Boolean | false | Shows loading state when true | | disableSearch | Boolean | false | Disable search functionality | | title | String | undefined | Table title | | subtitle | String | undefined | Table subtitle | | caption | String | undefined | Table caption for accessibility | | showOutlines | Boolean | true | Show cell outlines | | showStripes | Boolean | false | Show alternating row stripes | | enableOutlineFraming | Boolean | false | Enable outline framing | | enableRowNumbering | Boolean | false | Show row numbers | | allowRowSelection | Boolean | false | Enable row selection | | selectedRows | Array | [] | Array of selected row IDs | | disableRowSelect | Array | [] | Array of row IDs which is disabled selection | | filters | Array | [] | Array of filter definitions | | appliedFilters | Array | [] | Array of currently applied filters | | layout | String | 'default' | Table layout ('default' or 'full') | | enableReload | Boolean | false | Show reload button | | additionalContextButtons | Array | [] | Additional context buttons to show in the context menu | `} </Markdown> ## Events <Markdown> {` | Event Name | Payload | Description | |------------|---------|-------------| | paginationLimitChange | number | Emitted when page size changes | | paginationCurrentPageChange | number | Emitted when current page changes | | searchValueChange | string | Emitted when search value changes | | sortChange | { property: string, direction: string } | Emitted when sort changes | | selectionChange | { id: string, value: boolean } | Emitted when single row selection changes | | multipleSelectionChange | { selections: string[], value: boolean } | Emitted when multiple selection changes | | bulkEdit | string[] | Emitted when bulk edit is triggered | | bulkDelete | string[] | Emitted when bulk delete is triggered | | reload | void | Emitted when reload button is clicked | | changeShowOutlines | boolean | Emitted when outline visibility changes | | changeShowStripes | boolean | Emitted when stripe visibility changes | | changeOutlineFraming | boolean | Emitted when outline framing changes | | changeEnableRowNumbering | boolean | Emitted when row numbering changes | | contextSelect | { key: string, data: any } | Emitted when select a context menu item | `} </Markdown> ## Slots <Markdown> {` | Slot Name | Props | Description | |-----------|-------|-------------| | toolbar | - | Additional content for the toolbar area | | empty-state | - | Custom empty state content when no data is available | `} </Markdown> ## Filtering The data table supports a flexible filtering system. Filters can be defined using the following structure: ```typescript interface Filter { id: string; label: string; type: { id: string; options: Array<{ id: string; label: string; }>; }; } ``` Example of setting up filters: ```html <template> <mt-data-table :data-source="dataSource" :columns="columns" :filters="filters" :applied-filters="appliedFilters" @update:appliedFilters="updateFilters" /> </template> <script setup> const filters = [ { id: "status", label: "Status", type: { id: "select", options: [ { id: "active", label: "Active" }, { id: "inactive", label: "Inactive" }, ], }, }, ]; const appliedFilters = ref([]); const updateFilters = (newFilters) => { appliedFilters.value = newFilters; }; </script> ``` ## Loading States The data table provides built-in loading state handling with skeleton loading: ```html <template> <mt-data-table :data-source="dataSource" :columns="columns" :is-loading="isLoading" /> </template> <script setup> const isLoading = ref(false); const fetchData = async () => { isLoading.value = true; try { // Fetch your data } finally { isLoading.value = false; } }; </script> ``` ## Best Practices 1. **Column Positioning**: Use increments of 100 for column positions to leave room for inserting new columns later. 2. **Data Source**: Always provide a unique `id` field for each row in your data source. 3. **Performance**: Keep the data source size reasonable for the current page. Use server-side pagination for large datasets. 4. **Sorting**: Enable sorting only for columns where it makes sense. 5. **Filters**: Keep filter options concise and relevant to improve user experience. 6. **Loading States**: Always show loading state during data fetching to provide good UX. 7. **Accessibility**: Use the `caption` prop to provide a descriptive table summary for screen readers. ## Examples ### Basic Table <Canvas of={DataTableInteractiveStories.VisualTestRenderTable} /> ### Full Width Table <Canvas of={DataTableInteractiveStories.VisualTestRenderFullTable} /> ### Empty State <Canvas of={DataTableInteractiveStories.VisualTestRenderEmptyState} /> ### With Sticky Header <Canvas of={DataTableInteractiveStories.VisualTestRenderTableStickyHeader} />