@peakify/polaris-data-table-views
v1.4.1
Published
A complete data table component for Shopify Polaris IndexTable with filtering, sorting, pagination, URL sync, and view management. Optimized bundle size with zero external dependencies.
Downloads
201
Maintainers
Readme
@peakify/polaris-data-table-views
A complete data table component library for Shopify Polaris IndexTable with filtering, sorting, pagination, URL synchronization, and view management. Integrates seamlessly with @peakify/mongoose-url-query for server-side data fetching.
Table of Contents
Installation
npm install @peakify/polaris-data-table-views
# or
yarn add @peakify/polaris-data-table-viewsPeer Dependencies
This library requires:
@shopify/polaris: ^12.0.0 || ^13.0.0react: ^18.0.0mongoose: ^7.0.0 || ^8.0.0 (optional, only for server-side view management)
Quick Start
1. Basic Remote Data Table
import { ListTable } from '@peakify/polaris-data-table-views';
import { IndexTable } from '@shopify/polaris';
function UsersPage() {
return (
<ListTable
endpoint="/api/users"
queryKey="email"
headings={[{ title: 'Name' }, { title: 'Email' }, { title: 'Status' }]}
renderRowMarkup={(user) => (
<IndexTable.Row id={user._id} key={user._id}>
<IndexTable.Cell>{user.name}</IndexTable.Cell>
<IndexTable.Cell>{user.email}</IndexTable.Cell>
<IndexTable.Cell>{user.status}</IndexTable.Cell>
</IndexTable.Row>
)}
/>
);
}2. With View Management (Server-Side)
Client-side:
import { ListTable } from '@peakify/polaris-data-table-views';
function UsersPage() {
return (
<ListTable
endpoint="/api/users"
queryKey="email"
viewsEndpoint="/api/views"
headings={[...]}
renderRowMarkup={...}
/>
);
}Server-side (API route):
// pages/api/views.ts (Next.js) or routes/views.js (Express)
import { ViewModel } from '@peakify/polaris-data-table-views/server';
import {
serverGetViews,
serverCreateView,
serverUpdateView,
serverDeleteView,
serverRenameView,
} from '@peakify/polaris-data-table-views/server';
export default async function handler(req, res) {
const { path, action, name, oldName, newName } = req.query;
const ownerId = req.user?.id; // Optional: for user-specific views
switch (action) {
case 'createView':
await serverCreateView(path, name, req.body, ViewModel, ownerId);
return res.json({ success: true });
case 'updateView':
await serverUpdateView(path, name, req.body, ViewModel, ownerId);
return res.json({ success: true });
case 'deleteView':
await serverDeleteView(path, name, ViewModel, ownerId);
return res.json({ success: true });
case 'renameView':
await serverRenameView(path, oldName, newName, ViewModel, ownerId);
return res.json({ success: true });
default:
const views = await serverGetViews(path, ViewModel, ownerId);
return res.json({ items: views });
}
}Core Concepts
1. Data Source Modes
The library supports two data source modes:
- Remote Data Mode: Fetches data from an API endpoint (default)
- Local Data Mode: Filters/sorts/paginates data in-memory
2. View Management
Views are saved filter configurations that users can create, update, delete, and rename. Views are stored in MongoDB and can be:
- Shared: Available to all users (no
ownerId) - User-specific: Only visible to the owner (
ownerIdprovided)
3. URL Synchronization
By default, the table state (page, sort, filters, selected view) is synchronized with URL query parameters, allowing:
- Bookmarkable URLs
- Browser back/forward navigation
- Shareable filtered views
4. Filtering System
- Query Search: Text search on a specified field (
queryKey) - Custom Filters: Additional filters defined via
filtersprop - Applied Filters: Visual representation of active filters
Usage Patterns
Basic Usage with Remote Data
import { ListTable } from '@peakify/polaris-data-table-views';
<ListTable
endpoint="/api/products"
queryKey="name"
headings={[
{ title: 'Product', id: 'name' },
{ title: 'Price', id: 'price' },
]}
sortOptions={[
{ label: 'Name A-Z', value: 'name asc' },
{ label: 'Name Z-A', value: 'name desc' },
{ label: 'Price Low-High', value: 'price asc' },
]}
defaultSort={{ field: 'createdAt', direction: 'desc' }}
renderRowMarkup={(product) => (
<IndexTable.Row id={product._id}>
<IndexTable.Cell>{product.name}</IndexTable.Cell>
<IndexTable.Cell>${product.price}</IndexTable.Cell>
</IndexTable.Row>
)}
/>;Local Data Mode
When you have data already loaded and want to filter/sort/paginate in-memory:
const [products, setProducts] = useState([...]);
<ListTable
queryKey="name"
localData={products}
onlyLocalData={true}
headings={[...]}
renderRowMarkup={...}
/>Custom Fetch Function
For authentication, custom headers, or API wrappers:
const customFetch = async (url: string, options?: RequestInit) => {
return fetch(url, {
...options,
headers: {
...options?.headers,
'Authorization': `Bearer ${token}`,
},
});
};
<ListTable
endpoint="/api/products"
queryKey="name"
fetchFn={customFetch}
headings={[...]}
renderRowMarkup={...}
/>View Management
Client-Side Configuration
<ListTable
endpoint="/api/products"
queryKey="name"
viewsEndpoint="/api/views"
defaultViews={[
{
name: 'Active Products',
filters: { status: 'active' },
allowActions: ['update', 'delete', 'rename'], // Optional: restrict actions
},
]}
headings={[...]}
renderRowMarkup={...}
/>Server-Side API Route
// Next.js API route example
import { ViewModel } from '@peakify/polaris-data-table-views/server';
import {
serverGetViews,
serverCreateView,
serverUpdateView,
serverDeleteView,
serverRenameView,
} from '@peakify/polaris-data-table-views/server';
export default async function handler(req, res) {
const { method, query, body } = req;
const { path, action, name, oldName, newName } = query;
const ownerId = req.user?.id; // Get from your auth system
try {
switch (action) {
case 'createView':
if (method !== 'POST') return res.status(405).json({ error: 'Method not allowed' });
await serverCreateView(path, name, body, ViewModel, ownerId);
return res.json({ success: true });
case 'updateView':
if (method !== 'PUT') return res.status(405).json({ error: 'Method not allowed' });
await serverUpdateView(path, name, body, ViewModel, ownerId);
return res.json({ success: true });
case 'deleteView':
if (method !== 'GET') return res.status(405).json({ error: 'Method not allowed' });
await serverDeleteView(path, name, ViewModel, ownerId);
return res.json({ success: true });
case 'renameView':
if (method !== 'GET') return res.status(405).json({ error: 'Method not allowed' });
await serverRenameView(path, oldName, newName, ViewModel, ownerId);
return res.json({ success: true });
default:
// GET views
if (method !== 'GET') return res.status(405).json({ error: 'Method not allowed' });
const views = await serverGetViews(path, ViewModel, ownerId);
return res.json({ items: views });
}
} catch (error) {
console.error('View management error:', error);
return res.status(500).json({ error: error.message });
}
}Custom Filters
Add custom filter components (e.g., Select, DatePicker):
import { Select } from '@shopify/polaris';
<ListTable
endpoint="/api/products"
queryKey="name"
filters={[
{
key: 'status',
label: 'Status',
shortcut: true,
filter: {
Component: Select,
props: {
label: 'Status',
options: [
{ label: 'All', value: '' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' },
],
},
},
},
{
key: 'category',
label: 'Category',
shortcut: false,
filter: {
Component: Select,
props: {
label: 'Category',
options: [
{ label: 'All', value: '' },
{ label: 'Electronics', value: 'electronics' },
{ label: 'Clothing', value: 'clothing' },
],
},
},
},
]}
renderFilterLabel={(key, value) => {
if (key === 'status') {
return `Status: ${value}`;
}
if (key === 'category') {
return `Category: ${value}`;
}
return `${key}: ${value}`;
}}
headings={[...]}
renderRowMarkup={...}
/>Using Hooks Directly
For more control, use the hooks directly:
import { useDataSource } from '@peakify/polaris-data-table-views';
import { IndexTable, Card } from '@shopify/polaris';
function CustomTable() {
const {
items,
total,
loading,
state,
setPage,
setQueryValue,
setFilter,
setSort,
pagination,
} = useDataSource({
endpoint: '/api/products',
queryKey: 'name',
defaultSort: { field: 'createdAt', direction: 'desc' },
defaultLimit: 25,
});
return (
<Card>
<input
type="text"
placeholder="Search..."
onChange={(e) => setQueryValue(e.target.value)}
/>
<IndexTable
headings={[...]}
itemCount={items.length}
loading={loading}
pagination={{
hasNext: pagination.hasNext,
hasPrevious: pagination.hasPrevious,
onNext: pagination.onNext,
onPrevious: pagination.onPrevious,
label: pagination.label,
}}
>
{items.map((item) => (
<IndexTable.Row key={item._id}>
{/* Your row content */}
</IndexTable.Row>
))}
</IndexTable>
</Card>
);
}Server-Side Setup
1. Install and Connect Mongoose
// lib/mongodb.ts or similar
import mongoose from 'mongoose';
if (!mongoose.connection.readyState) {
await mongoose.connect(process.env.MONGODB_URI);
}2. Create API Route for Views
See View Management section above.
3. Create API Route for Data
Your data endpoint should accept query parameters from @billy/mongoose-url-query:
// pages/api/products.ts
import { buildQuery } from '@billy/mongoose-url-query';
import Product from '@/models/Product';
export default async function handler(req, res) {
const { page, limit, sort, filters } = buildQuery(req.query);
const query = Product.find();
// Apply filters
if (filters) {
// @billy/mongoose-url-query filters are already parsed
Object.entries(filters).forEach(([key, value]) => {
if (Array.isArray(value)) {
query.where(key).in(value);
} else {
query.where(key).equals(value);
}
});
}
// Apply sorting
if (sort) {
query.sort(sort);
}
// Apply pagination
const skip = (page - 1) * limit;
query.skip(skip).limit(limit);
const [items, total] = await Promise.all([
query.exec(),
Product.countDocuments(query.getQuery()),
]);
res.json({ items, total, page, limit });
}API Reference
ListTable Component
Props
| Prop | Type | Required | Default | Description |
| --------------------------- | ------------------------------------------------------ | -------- | ---------------- | --------------------------------------------------------- |
| endpoint | string | No* | - | API endpoint for data fetching (required for remote mode) |
| queryKey | string | Yes | - | Field name for text search |
| headings | IndexTableHeading[] | Yes | - | Table column headers |
| renderRowMarkup | (item, idx, selectedResources, context) => ReactNode | Yes | - | Function to render each row |
| localData | T[] | No | - | Array of data for local mode |
| onlyLocalData | boolean | No | false | Enable local data mode |
| viewsEndpoint | string | No | - | API endpoint for view management |
| defaultViews | ListTableView[] | No | [] | Default views to show |
| views | ListTableView[] | No | - | Controlled views (overrides fetching) |
| filters | ListTableFilter[] | No | [] | Custom filter definitions |
| sortOptions | IndexFiltersProps['sortOptions'] | No | - | Sort dropdown options |
| defaultSort | { field: string; direction: 'asc' \| 'desc' } | No | - | Default sort configuration |
| limit | number | No | 50 | Items per page |
| selectable | boolean | No | false | Enable row selection |
| bulkActions | BulkActionsProps['actions'] | No | - | Bulk action buttons |
| promotedBulkActions | BulkActionsProps['promotedActions'] | No | - | Promoted bulk actions |
| condensed | boolean | No | false | Use condensed table layout |
| showBorder | boolean | No | true | Show card border |
| showFilter | boolean | No | true | Show filter UI |
| showPagination | boolean | No | true | Show pagination controls |
| emptyState | ReactNode | No | - | Custom empty state component |
| fetchFn / fetchFunction | (url, options?) => Promise<Response> | No | defaultFetch | Custom fetch function |
| syncWithUrl | boolean | No | true | Sync state with URL params |
| queryPlaceholder | string | No | 'Filter items' | Search input placeholder |
| renderFilterLabel | (key, value) => string | No | - | Custom filter label renderer |
| resourceName | { singular: string; plural: string } | No | - | Resource names for bulk actions |
| t | (key, options?) => string | No | defaultT | Translation function |
| onDataChange | (data: ListTableData) => void | No | - | Callback when data changes |
| setListTableData | Dispatch<SetStateAction<ListTableData>> | No | - | State setter for data |
| error | Error | No | - | Error to display |
| loadingComponent | ReactNode | No | - | Custom loading component |
| abbreviated | string | No | - | Abbreviated response format |
ListTableView Type
type ListTableView = {
_id?: string;
name: string;
filters: {
queryValue?: string;
[key: string]: any;
};
allowActions?: VIEW_ACTIONS[]; // ['createView', 'updateView', 'deleteView', 'renameView', 'duplicateView']
};ListTableFilter Type
type ListTableFilter = {
key: string;
label: string;
shortcut: boolean; // Show in shortcut bar
filter:
| {
Component: React.ComponentType<any>;
props: any;
}
| ReactNode;
};useDataSource Hook
Options
interface UseDataSourceOptions<T> {
endpoint: string;
queryKey: string;
defaultSort?: { field: string; direction: 'asc' | 'desc' };
defaultLimit?: number;
defaultViews?: ViewDefinition[];
syncWithUrl?: boolean;
localData?: T[];
abbreviated?: boolean;
transformResponse?: (response: unknown) => QueryResult<T>;
fetchFn?: (url: string, options?: RequestInit) => Promise<unknown>;
debounceMs?: number;
}Return Value
interface UseDataSourceReturn<T> {
// State
state: QueryState;
items: T[];
total: number;
loading: boolean;
firstLoad: boolean;
error: Error | null;
// Actions
setPage: (page: number) => void;
setQueryValue: (value: string) => void;
setFilter: (key: string, value: any) => void;
setFilters: (filters: Record<string, any>) => void;
clearFilters: () => void;
setSort: (sort: SortDefinition | null) => void;
setSelectedView: (index: number) => void;
setViewSelected: (viewNameOrId: string | null) => void;
refresh: () => void;
// Polaris helpers
tabs: IndexFiltersProps['tabs'];
sortOptions: IndexFiltersProps['sortOptions'];
sortSelected: string[];
onSort: (selected: string[]) => void;
// Pagination helpers
pagination: {
page: number;
totalPages: number;
hasPrevious: boolean;
hasNext: boolean;
onPrevious: () => void;
onNext: () => void;
goToPage: (page: number) => void;
label: string;
};
}Server Utilities
serverGetViews
function serverGetViews(
path: string,
ViewModel: Model<IView>,
ownerId?: string,
select?: (keyof IView)[]
): Promise<IView[]>;serverCreateView
function serverCreateView(
path: string,
name: string,
filters: Record<string, any>,
ViewModel: Model<IView>,
ownerId?: string
): Promise<void>;serverUpdateView
function serverUpdateView(
path: string,
name: string,
filters: Record<string, any>,
ViewModel: Model<IView>,
ownerId?: string
): Promise<void>;serverDeleteView
function serverDeleteView(
path: string,
name: string,
ViewModel: Model<IView>,
ownerId?: string
): Promise<void>;serverRenameView
function serverRenameView(
path: string,
oldName: string,
newName: string,
ViewModel: Model<IView>,
ownerId?: string
): Promise<void>;Advanced Usage
Custom View Models
Create a custom Mongoose model with additional fields:
import { createViewModel } from '@peakify/polaris-data-table-views/server';
import { Schema } from 'mongoose';
const CustomViewModel = createViewModel({
modelName: 'CustomView',
collectionName: 'custom_views',
schemaOptions: {
description: { type: String },
isPublic: { type: Boolean, default: false },
tags: [{ type: String }],
metadata: { type: Schema.Types.Mixed },
},
additionalIndexes: [{ fields: { isPublic: 1 } }, { fields: { tags: 1 } }],
});
// Use in your API routes
const views = await serverGetViews('/admin/products', CustomViewModel, userId);URL Synchronization
The library automatically syncs state with URL parameters:
?page=2- Current page?sort=name|asc- Sort field and direction?query=search+term- Search query?filter_status=active- Custom filter values?viewSelected=My+View- Selected view name/ID
To disable URL sync:
<ListTable
syncWithUrl={false}
// ... other props
/>Transform Response
Transform API responses to match expected format:
<ListTable
endpoint="/api/products"
queryKey="name"
transformResponse={(response) => {
// Transform from { data: [...], count: 100 } to { items: [...], total: 100 }
return {
items: response.data || [],
total: response.count || 0,
};
}}
headings={[...]}
renderRowMarkup={...}
/>Error Handling
Display custom error messages:
const [error, setError] = useState<Error | null>(null);
<ListTable
endpoint="/api/products"
queryKey="name"
error={error}
headings={[...]}
renderRowMarkup={...}
/>Or handle errors in custom fetch:
const customFetch = async (url: string, options?: RequestInit) => {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
return response;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
};Exports
Client-Side Exports
// Main component
import { ListTable } from '@peakify/polaris-data-table-views';
// Hooks
import { useDataSource, useSelection, usePagination } from '@peakify/polaris-data-table-views';
// Types
import type {
ListTableProps,
ListTableData,
ListTableView,
ListTableFilter,
} from '@peakify/polaris-data-table-views/types';
// Constants
import { VIEW_ACTIONS } from '@peakify/polaris-data-table-views/types';
import { TABLE_ITEM_LIST_LIMITATION } from '@peakify/polaris-data-table-views/constants';
// Utils
import { defaultFetch, defaultT } from '@peakify/polaris-data-table-views';Server-Side Exports
// Models
import { ViewModel } from '@peakify/polaris-data-table-views/server';
import {
createViewModel,
baseViewSchemaDefinition,
createBaseViewIndexes,
} from '@peakify/polaris-data-table-views/server';
// Server utilities
import {
serverGetViews,
serverCreateView,
serverUpdateView,
serverDeleteView,
serverRenameView,
} from '@peakify/polaris-data-table-views/server';TypeScript Support
Full TypeScript support is included. All types are exported and can be imported:
import type {
ListTableProps,
UseDataSourceOptions,
UseDataSourceReturn,
QueryState,
SortDefinition,
ViewDefinition,
} from '@peakify/polaris-data-table-views';Best Practices
- Always provide
queryKey: Required for text search functionality - Use
viewsEndpointfor persistent views: Enables save/load functionality - Implement proper error handling: Use
errorprop or customfetchFn - Optimize with
abbreviated: For large datasets, request only needed fields - Use
onlyLocalDatafor small datasets: Avoids unnecessary API calls - Customize
renderFilterLabel: Provides better UX for applied filters - Set
defaultSort: Improves initial load performance - Use
syncWithUrl={false}: Only if you don't need bookmarkable URLs
License
MIT
