@zango-core/crud
v1.0.12
Published
Unified React CRUD components library for tables, forms and workflows.
Downloads
588
Readme
CRUD Library Composable Architecture - Complete Guide
Overview
This document serves as the complete reference for implementing the composable CRUD architecture. The library transforms from a monolithic table component into a fully composable, context-driven system where any combination of default and custom components work seamlessly together.
Core Architecture Philosophy
1. Separation of Concerns
- Data Logic: Handled by hooks and context (API calls, state management)
- UI Logic: Handled by components (rendering, user interactions)
- State Management: Centralized in React Context with perfect synchronization
2. Component Independence with State Sharing
- Each component can be default or custom
- All components share the same data/filter/pagination state via Context
- Perfect synchronization: Filter changes automatically update all views
- No prop drilling required
3. Composable Building Blocks
- Mix and match any combination of components
- Create completely custom layouts while reusing data logic
- Gradual migration path from existing implementations
Implementation Plan Summary
Phase 1: Core Context & Provider System
- Create
TableContextfor centralized state management - Implement
TableProvidercomponent with all business logic - Create
useTableContexthook for component consumption
Phase 2: Component Refactoring
- Refactor existing components to use context instead of props
- Remove prop drilling from all components
- Maintain backward compatibility
Phase 3: New Composable Components
- Create layout components for different use cases
- Implement specialized hooks for granular control
- Add render prop components for maximum flexibility
Phase 4: Enhanced Export System
- Export 50+ items for consumer use
- Maintain backward compatibility
- Provide comprehensive TypeScript definitions
Context Structure
Complete TableContext Definition
interface TableContextValue {
// Data Management
data: any[];
isLoading: boolean;
isFetching: boolean;
error: Error | null;
totalCount: number;
// Search State
searchTerm: string;
setSearchTerm: (term: string) => void;
debouncedSearchTerm: string;
// Filter State
filterObj: Record<string, any>;
setFilterObj: (filters: Record<string, any>) => void;
debouncedFilterObj: Record<string, any>;
activeFilterCount: number;
clearFilters: () => void;
// Pagination State
currentPage: number;
setCurrentPage: (page: number) => void;
totalPages: number;
rowsPerPage: number;
setRowsPerPage: (size: number) => void;
// Table Instance (for sorting, columns)
table: Table<any> | null;
columns: ColumnDef<any>[];
// Selection State
selectedRows: any[];
setSelectedRows: (rows: any[]) => void;
selectRow: (row: any) => void;
selectAll: () => void;
clearSelection: () => void;
// View State (for column management)
columnVisibility: Record<string, boolean>;
setColumnVisibility: (visibility: Record<string, boolean>) => void;
columnOrder: string[];
setColumnOrder: (order: string[]) => void;
columnPinning: ColumnPinningState;
setColumnPinning: (pinning: ColumnPinningState) => void;
sorting: SortingState;
setSorting: (sorting: SortingState) => void;
// Actions
refresh: () => void;
refetch: () => void;
invalidateCache: () => void;
// Configuration
apiUrl: string;
tableId: string;
config: TableConfig;
features: TableFeatures;
}State Synchronization Flow
User Interaction → Context State Update → Data Refetch → All Components Re-render
↓
Filter Change → filterObj changes → useTableData hook → All Views Update
Search Change → searchTerm changes → API call → Cards/Table/List Update
Page Change → currentPage changes → New data fetch → All Components UpdateLibrary Exports (50+ Items)
Context System (4 items)
TableProvider- Main provider componentuseTableContext- Hook to consume contextTableContext- Raw context (rarely used directly)TableContextValue- TypeScript interface
Hooks (15 items)
useTableData- Main data management hookuseTableSearch- Search state and actionsuseTableFilters- Filter state and actionsuseTablePagination- Pagination state and actionsuseTableSelection- Row selection managementuseTableColumns- Column visibility/orderinguseTableSorting- Sorting state managementuseTableActions- Action methods (refresh, etc.)useTableEvents- Event system integrationuseTablePersistence- State persistenceuseTableConfig- Configuration managementuseTableMetadata- Table metadata accessuseTableQuery- Direct query accessuseTableCache- Cache managementuseTableOptimisticUpdates- Optimistic UI updates
Layout Components (8 items)
TableLayout- Default complete layoutCompactLayout- Minimal space layoutSplitLayout- Side-by-side layoutCustomLayout- Render prop basedGridLayout- Card/grid optimized layoutListLayout- List view optimizedDashboardLayout- Dashboard styleMobileLayout- Mobile optimized
UI Components (15 items)
TableSearch- Search input componentTableFilters- Filter panel componentTablePagination- Pagination controlsTableUtilities- Action button barTableHeader- Header sectionTableViewOptions- Column/view optionsTableBulkActions- Bulk operation controlsTableExportButton- Export functionalityTableRefreshButton- Refresh data buttonTableRowSelector- Row selection checkboxTableSortButton- Column sort controlTableFilterChips- Active filter displayTableEmptyState- No data stateTableLoadingState- Loading indicatorTableErrorState- Error handling display
Render Prop Components (5 items)
TableData- Data render propTableControls- Controls render propTableView- View render propTableState- State render propTableActions- Actions render prop
Utilities (10 items)
buildApiUrl- URL construction helperformatTableData- Data formattingvalidateFilters- Filter validationcreateTableColumns- Column generationhandleTableEvents- Event handlingserializeTableState- State serializationparseTableConfig- Configuration parsingoptimizeTableQuery- Query optimizationcacheTableData- Data cachingdebugTableState- Development debugging
Consumer Usage Examples
Example 1: Default Experience (Backward Compatible)
// Updated to use new CrudHandler
function CustomerPage() {
return <CrudHandler api_endpoint="/api/customers" />;
}Example 2: Custom Card View with Default Controls
function CustomerCardPage() {
return (
<TableProvider apiUrl="/api/customers">
{/* Use default search component */}
<TableSearch />
{/* Use default filter component */}
<TableFilters />
{/* Custom card view using context data */}
<CustomCardGrid />
{/* Use default pagination */}
<TablePagination />
</TableProvider>
);
}
function CustomCardGrid() {
const { data, isLoading } = useTableContext();
if (isLoading) return <div>Loading cards...</div>;
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.map(customer => (
<CustomerCard key={customer.id} customer={customer} />
))}
</div>
);
}Example 3: Custom Layout with Mixed Components
function ProductDashboard() {
return (
<TableProvider apiUrl="/api/products">
<div className="dashboard-layout">
{/* Custom sidebar with default filter logic */}
<aside className="sidebar">
<h2>Filters</h2>
<TableFilters />
</aside>
<main className="content">
{/* Custom header */}
<header className="flex justify-between items-center mb-4">
<h1>Products</h1>
<div className="flex gap-2">
<TableSearch />
<TableUtilities />
</div>
</header>
{/* Custom kanban view */}
<ProductKanbanBoard />
{/* Default pagination */}
<TablePagination />
</main>
</div>
</TableProvider>
);
}Example 4: Completely Custom with Hooks
function CustomProductManager() {
const {
data,
isLoading,
searchTerm,
setSearchTerm,
filterObj,
setFilterObj,
currentPage,
setCurrentPage,
totalPages,
refresh
} = useTableData({ apiUrl: '/api/products' });
return (
<div className="custom-manager">
{/* Completely custom search */}
<div className="search-section">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search products..."
className="custom-search"
/>
</div>
{/* Custom filter buttons */}
<div className="filter-buttons">
<button
onClick={() => setFilterObj({...filterObj, category: 'electronics'})}
className={filterObj.category === 'electronics' ? 'active' : ''}
>
Electronics
</button>
<button
onClick={() => setFilterObj({...filterObj, category: 'clothing'})}
className={filterObj.category === 'clothing' ? 'active' : ''}
>
Clothing
</button>
</div>
{/* Custom grid view */}
<div className="product-grid">
{data.map(product => (
<CustomProductCard key={product.id} product={product} />
))}
</div>
{/* Custom pagination */}
<div className="pagination">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage(currentPage - 1)}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(currentPage + 1)}
>
Next
</button>
</div>
</div>
);
}Example 5: Render Props Pattern
function OrderManagement() {
return (
<TableData apiUrl="/api/orders">
{({ data, isLoading, filters, pagination, actions }) => (
<div className="order-management">
{/* Custom controls using render prop data */}
<OrderControls
filters={filters}
onRefresh={actions.refresh}
/>
{/* Custom view using data */}
<OrderTimeline
orders={data}
loading={isLoading}
/>
{/* Custom pagination using pagination data */}
<OrderPagination
current={pagination.currentPage}
total={pagination.totalPages}
onChange={pagination.setCurrentPage}
/>
</div>
)}
</TableData>
);
}Example 6: Advanced Custom Filter Integration
function AdvancedCustomerFilters() {
const { filterObj, setFilterObj, activeFilterCount, clearFilters } = useTableFilters();
const handleLocationFilter = (location) => {
setFilterObj({
...filterObj,
location: location
});
};
const handleDateRangeFilter = (startDate, endDate) => {
setFilterObj({
...filterObj,
dateRange: [startDate, endDate]
});
};
return (
<div className="advanced-filters">
{/* Custom location filter */}
<div className="filter-group">
<label>Location</label>
<select
value={filterObj.location || ''}
onChange={(e) => handleLocationFilter(e.target.value)}
>
<option value="">All Locations</option>
<option value="north">North Region</option>
<option value="south">South Region</option>
</select>
</div>
{/* Custom date range filter */}
<div className="filter-group">
<label>Date Range</label>
<CustomDateRangePicker
onDateChange={handleDateRangeFilter}
value={filterObj.dateRange}
/>
</div>
{/* Filter status and clear */}
<div className="filter-status">
{activeFilterCount > 0 && (
<>
<span>{activeFilterCount} filters active</span>
<button onClick={clearFilters}>Clear All</button>
</>
)}
</div>
</div>
);
}Example 7: Mobile-Optimized Layout
function MobileCustomerList() {
return (
<TableProvider apiUrl="/api/customers">
<MobileLayout>
{/* Mobile-friendly search */}
<div className="mobile-header">
<TableSearch placeholder="Search customers..." />
</div>
{/* Collapsible filters */}
<MobileFilterDrawer>
<TableFilters />
</MobileFilterDrawer>
{/* List view for mobile */}
<CustomMobileList />
{/* Infinite scroll pagination */}
<InfiniteScrollPagination />
</MobileLayout>
</TableProvider>
);
}Example 8: Split View Layout
function CustomerDetailSplit() {
const [selectedCustomer, setSelectedCustomer] = useState(null);
return (
<TableProvider apiUrl="/api/customers">
<div className="split-layout">
{/* Left panel - List */}
<div className="left-panel">
<TableSearch />
<CustomCustomerList
onCustomerSelect={setSelectedCustomer}
selectedId={selectedCustomer?.id}
/>
<TablePagination />
</div>
{/* Right panel - Detail */}
<div className="right-panel">
{selectedCustomer ? (
<CustomerDetail customer={selectedCustomer} />
) : (
<div>Select a customer to view details</div>
)}
</div>
</div>
</TableProvider>
);
}Implementation Files to Create
Core System Files
src/table/context/
├── TableContext.tsx # Context definition
├── TableProvider.tsx # Provider with all logic
├── useTableContext.tsx # Context consumer hook
└── index.ts # Context exports
src/table/hooks/
├── useTableData.tsx # Main data hook
├── useTableSearch.tsx # Search management
├── useTableFilters.tsx # Filter management
├── useTablePagination.tsx # Pagination management
├── useTableSelection.tsx # Selection management
└── [10 more specialized hooks]
src/table/layouts/
├── TableLayout.tsx # Default layout
├── CompactLayout.tsx # Space-efficient
├── SplitLayout.tsx # Side-by-side
├── CustomLayout.tsx # Render props
└── [4 more layout variants]
src/table/render-props/
├── TableData.tsx # Data render prop
├── TableControls.tsx # Controls render prop
├── TableView.tsx # View render prop
└── [2 more render prop components]Migration Strategy
Phase 1: Maintain Backward Compatibility
- Existing
<Table apiUrl={url} />is replaced by<CrudHandler api_endpoint={url} /> - No breaking changes for current consumers
- Internal refactoring only
Phase 2: Introduce New APIs
- Add
<TableProvider>pattern alongside existing API - Provide new hooks for custom development
- Document migration benefits
Phase 3: Full Composable Ecosystem
- Complete library of composable components
- Comprehensive examples and documentation
- Performance optimizations
Key Benefits
- Perfect State Synchronization: All components stay in sync automatically
- Component Agnostic: Mix default and custom components freely
- Gradual Migration: Adopt new patterns incrementally
- Type Safety: Full TypeScript support throughout
- Performance: Optimized re-rendering and API calls
- Developer Experience: Intuitive APIs with comprehensive examples
Development Timeline
- Week 1: Context system and provider setup
- Week 2: Refactor existing components
- Week 3: New hooks and layout components
- Week 4: Render props and optimization
- Week 5: Documentation and examples
This architecture enables infinite customization possibilities while maintaining the simplicity of the original API for basic use cases.
Array Field Row-Level Controls
The form system now supports row-level button control for array fields, allowing you to control the visibility of action buttons (remove, copy, move up/down) and set read-only state on individual rows within array fields.
Features
- Per-row button visibility: Control which action buttons appear for each row
- Read-only rows: Make specific rows non-editable while keeping others editable
- Backward compatibility: Existing forms continue working unchanged
- Fallback support: Row-level flags take precedence over field-level
ui:options
Row-Level Flags
Add these flags to individual row data in your array field defaults:
| Flag | Type | Description |
|------|------|-------------|
| _hasRemove | boolean | Show/hide the remove (trash) button for this row |
| _hasCopy | boolean | Show/hide the copy button for this row |
| _hasMoveUp | boolean | Show/hide the move up (arrow up) button for this row |
| _hasMoveDown | boolean | Show/hide the move down (arrow down) button for this row |
| _isReadOnly | boolean | Make the entire row read-only (disables all form fields and buttons) |
Usage Examples
Basic Example: Hide Remove Button
{
"functional_team_members": {
"type": "array",
"default": [
{
"department": "local_safety",
"name": "Rajat",
"email": "[email protected]",
"_hasRemove": false // This row cannot be removed
},
{
"department": "commercial_marketing",
// No flags = uses field-level settings or defaults
}
]
}
}Advanced Example: Mixed Row States
{
"business_team_members": {
"type": "array",
"title": "Business Team Members",
"default": [
{
// Read-only system user - cannot be edited or removed
"department": "system_admin",
"name": "System Administrator",
"email": "[email protected]",
"_isReadOnly": true,
"_hasRemove": false,
"_hasCopy": false,
"_hasMoveUp": false,
"_hasMoveDown": false
},
{
// Required user - can be edited but not removed
"department": "business_reviewer",
"name": "John Doe",
"email": "[email protected]",
"_hasRemove": false,
"_hasCopy": true // Allow copying this template
},
{
// Standard editable row with all controls
"department": "program_owner"
// Uses default field-level settings
}
]
}
}Complete Form Schema Example
{
"json_schema": {
"type": "object",
"properties": {
"team_members": {
"type": "array",
"title": "Team Members",
"minItems": 1,
"items": {
"type": "object",
"required": ["name", "email", "role"],
"properties": {
"name": {"type": "string", "title": "Name"},
"email": {"type": "string", "title": "Email", "format": "email"},
"role": {
"type": "string",
"title": "Role",
"enum": ["admin", "user", "viewer"]
}
}
},
"default": [
{
"name": "System Admin",
"email": "[email protected]",
"role": "admin",
"_isReadOnly": true,
"_hasRemove": false
},
{
"name": "Template User",
"email": "[email protected]",
"role": "user",
"_hasCopy": true,
"_hasRemove": false
}
]
}
}
},
"ui_schema": {
"team_members": {
"ui:options": {
"orderable": true, // Field-level default: allow reordering
"addable": true, // Field-level default: allow adding
"removable": true // Field-level default: allow removing
}
}
}
}Precedence Rules
The system follows this precedence order:
- Row-level flags (highest priority)
- Field-level
ui:options(fallback) - System defaults (lowest priority)
// Row-level flag overrides field-level setting
{
"ui_schema": {
"members": {
"ui:options": {
"removable": true // Field says "allow remove"
}
}
},
"default": [
{
"name": "John",
"_hasRemove": false // Row-level override: this row cannot be removed
}
]
}Read-Only Row Behavior
When _isReadOnly: true is set on a row:
- Visual changes: Row becomes slightly transparent (opacity-75)
- Form fields disabled: All input fields become non-interactive
- Buttons disabled: All action buttons are disabled and grayed out
- Pointer events disabled: No mouse interactions allowed on form fields
{
"default": [
{
"name": "Protected User",
"email": "[email protected]",
"_isReadOnly": true // This entire row cannot be edited
}
]
}Data Cleaning
The system automatically removes all internal flags before form submission:
_hasRemove,_hasCopy,_hasMoveUp,_hasMoveDown,_isReadOnlyare stripped- Clean data is sent to both custom
on_submithandlers and API endpoints - No manual cleaning required
Migration Notes
- Existing forms: Continue working unchanged
- No breaking changes: All current functionality preserved
- Gradual adoption: Add row-level flags only where needed
- Field-level fallback: Existing
ui:optionsstill work as before
Implementation Details
The feature uses:
- Lucide React icons:
ArrowUp,ArrowDown,Copy,Trash - Utility functions: Automatic flag extraction and data cleaning
- Context preservation: Maintains existing RJSF architecture
- Performance optimized: Minimal re-rendering impact
