TablixJS Plugin Architecture & Extensibility
July 23, 2025 · View on GitHub
This document explains how TablixJS is designed for extensibility and how to create plugins for additional functionality.
Architecture Overview
TablixJS follows a modular architecture with clear separation of concerns:
Table (Core)
├── DataManager (Data handling)
├── EventManager (Event system)
├── ColumnManager (Column formatting)
├── Renderer (UI rendering)
├── PaginationManager (Pagination logic)
├── SortingManager (Sorting logic)
├── FilterManager (Filtering logic)
└── FilterUI (Filter interface)
Plugin Development Guidelines
1. Manager Pattern
All major functionality is implemented as "Manager" classes that:
- Take the main
Tableinstance as the first parameter - Have their own configuration options
- Integrate via the EventManager for loose coupling
- Are conditionally initialized based on options
2. Event-Driven Architecture
Managers communicate through events:
// Trigger events
this.table.eventManager.trigger('eventName', payload);
// Listen to events
this.table.eventManager.on('eventName', callback);
3. Renderer Integration
UI components integrate with the main Renderer:
// In Renderer.renderTable()
if (this.table.filterUI) {
this.table.filterUI.renderFilterIcons();
}
Example: Creating a Column Resizing Plugin
Here's how to create a column resizing plugin following TablixJS patterns:
Step 1: Create ResizeManager
// src/core/ResizeManager.js
export default class ResizeManager {
constructor(table, options = {}) {
this.table = table;
this.options = {
enabled: true,
minWidth: 50,
maxWidth: 500,
persistState: true,
...options
};
this.columnWidths = new Map();
this.isResizing = false;
this.resizeData = null;
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.init();
}
init() {
// Load saved column widths
if (this.options.persistState) {
this.loadColumnWidths();
}
// Listen for table render events
this.table.eventManager.on('afterRender', () => {
this.renderResizeHandles();
});
}
renderResizeHandles() {
const headers = this.table.container.querySelectorAll('.tablix-th');
headers.forEach((header, index) => {
// Skip if resize handle already exists
if (header.querySelector('.tablix-resize-handle')) return;
const resizeHandle = document.createElement('div');
resizeHandle.className = 'tablix-resize-handle';
resizeHandle.addEventListener('mousedown', (e) => {
this.handleMouseDown(e, header, index);
});
header.style.position = 'relative';
header.appendChild(resizeHandle);
// Apply saved width if available
const columnName = header.dataset.column;
if (this.columnWidths.has(columnName)) {
header.style.width = this.columnWidths.get(columnName) + 'px';
}
});
}
handleMouseDown(e, header, columnIndex) {
e.preventDefault();
this.isResizing = true;
this.resizeData = {
header,
columnIndex,
startX: e.clientX,
startWidth: header.offsetWidth
};
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
// Trigger event
this.table.eventManager.trigger('beforeColumnResize', {
columnIndex,
columnName: header.dataset.column,
currentWidth: header.offsetWidth
});
}
handleMouseMove(e) {
if (!this.isResizing || !this.resizeData) return;
const { header, startX, startWidth } = this.resizeData;
const deltaX = e.clientX - startX;
const newWidth = Math.max(
this.options.minWidth,
Math.min(this.options.maxWidth, startWidth + deltaX)
);
header.style.width = newWidth + 'px';
// Update corresponding table cells
this.updateColumnWidth(this.resizeData.columnIndex, newWidth);
}
handleMouseUp() {
if (!this.isResizing) return;
const { header, columnIndex } = this.resizeData;
const newWidth = header.offsetWidth;
const columnName = header.dataset.column;
// Save new width
this.columnWidths.set(columnName, newWidth);
if (this.options.persistState) {
this.saveColumnWidths();
}
// Trigger event
this.table.eventManager.trigger('afterColumnResize', {
columnIndex,
columnName,
newWidth
});
// Cleanup
this.isResizing = false;
this.resizeData = null;
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
}
updateColumnWidth(columnIndex, width) {
// Update all cells in this column
const table = this.table.container.querySelector('.tablix-table');
const cells = table.querySelectorAll(`td:nth-child(${columnIndex + 1})`);
cells.forEach(cell => {
cell.style.width = width + 'px';
});
}
// Public API methods
setColumnWidth(columnName, width) {
this.columnWidths.set(columnName, width);
// Trigger re-render or apply immediately
this.renderResizeHandles();
}
getColumnWidth(columnName) {
return this.columnWidths.get(columnName);
}
resetColumnWidths() {
this.columnWidths.clear();
if (this.options.persistState) {
localStorage.removeItem('tablixColumnWidths');
}
}
saveColumnWidths() {
const widthsObj = Object.fromEntries(this.columnWidths);
localStorage.setItem('tablixColumnWidths', JSON.stringify(widthsObj));
}
loadColumnWidths() {
const saved = localStorage.getItem('tablixColumnWidths');
if (saved) {
const widthsObj = JSON.parse(saved);
this.columnWidths = new Map(Object.entries(widthsObj));
}
}
}
Step 2: Create ResizeUI (if needed)
// src/core/ResizeUI.js
export default class ResizeUI {
constructor(resizeManager) {
this.resizeManager = resizeManager;
this.table = resizeManager.table;
}
renderResizeIndicator() {
// Create visual feedback during resize
if (!this.indicator) {
this.indicator = document.createElement('div');
this.indicator.className = 'tablix-resize-indicator';
document.body.appendChild(this.indicator);
}
}
showResizeIndicator(x, width) {
if (this.indicator) {
this.indicator.style.left = x + 'px';
this.indicator.style.width = width + 'px';
this.indicator.style.display = 'block';
}
}
hideResizeIndicator() {
if (this.indicator) {
this.indicator.style.display = 'none';
}
}
}
Step 3: Create CSS Styles
/* src/styles/resize-core.css */
.tablix-resize-handle {
position: absolute;
top: 0;
right: -2px;
width: 4px;
height: 100%;
cursor: col-resize;
background: transparent;
border-right: 2px solid transparent;
transition: border-color 0.2s ease;
}
.tablix-resize-handle:hover {
border-right-color: var(--tablix-btn-active-color, #007bff);
}
.tablix-resize-indicator {
position: fixed;
top: 0;
height: 100vh;
background: var(--tablix-btn-active-color, #007bff);
opacity: 0.3;
pointer-events: none;
z-index: 9999;
display: none;
}
/* Resizing state */
.tablix-table.tablix-resizing {
user-select: none;
}
.tablix-table.tablix-resizing * {
cursor: col-resize !important;
}
Step 4: Integration with Main Table
// Add to Table.js imports
import ResizeManager from './ResizeManager.js';
import ResizeUI from './ResizeUI.js';
// Add to Table constructor options
this.options = {
// ...existing options...
resize: {
enabled: false,
minWidth: 50,
maxWidth: 500,
persistState: true
},
...options
};
// Add to Table constructor initialization
// Initialize resizing if enabled
if (this.options.resize && this.options.resize.enabled !== false) {
this.resizeManager = new ResizeManager(this, this.options.resize);
this.resizeUI = new ResizeUI(this.resizeManager);
}
// Add public API methods to Table class
setColumnWidth(columnName, width) {
if (this.resizeManager) {
this.resizeManager.setColumnWidth(columnName, width);
}
}
getColumnWidth(columnName) {
return this.resizeManager ? this.resizeManager.getColumnWidth(columnName) : null;
}
resetColumnWidths() {
if (this.resizeManager) {
this.resizeManager.resetColumnWidths();
}
}
Creating Other Plugin Types
1. Data Export Plugin
export default class ExportManager {
constructor(table, options = {}) {
this.table = table;
this.options = {
formats: ['csv', 'json', 'excel'],
filename: 'table-export',
includeHeaders: true,
...options
};
}
exportData(format = 'csv') {
const data = this.table.dataManager.getData();
const columns = this.table.columnManager.getColumns();
switch (format) {
case 'csv':
return this.exportCSV(data, columns);
case 'json':
return this.exportJSON(data);
case 'excel':
return this.exportExcel(data, columns);
default:
throw new Error(`Unsupported export format: ${format}`);
}
}
exportCSV(data, columns) {
// CSV export implementation
}
exportJSON(data) {
// JSON export implementation
}
exportExcel(data, columns) {
// Excel export implementation (requires library)
}
}
2. Inline Editing Plugin
export default class EditManager {
constructor(table, options = {}) {
this.table = table;
this.options = {
enabled: true,
editableColumns: [],
saveOnBlur: true,
validators: {},
...options
};
this.editingCell = null;
this.originalValue = null;
}
startEdit(rowIndex, columnName) {
// Begin editing a cell
this.table.eventManager.trigger('beforeEdit', {
rowIndex,
columnName,
currentValue: this.getCurrentValue(rowIndex, columnName)
});
// Implementation...
}
saveEdit() {
// Save edited value
this.table.eventManager.trigger('afterEdit', {
rowIndex: this.editingCell.rowIndex,
columnName: this.editingCell.columnName,
oldValue: this.originalValue,
newValue: this.getEditedValue()
});
}
cancelEdit() {
// Cancel editing and restore original value
}
}
3. Drag & Drop Plugin
export default class DragDropManager {
constructor(table, options = {}) {
this.table = table;
this.options = {
rowDrag: true,
columnDrag: true,
...options
};
}
enableRowDragging() {
// Implementation for row reordering
}
enableColumnDragging() {
// Implementation for column reordering
}
}
Plugin Best Practices
1. Follow Manager Pattern
- Constructor takes
(table, options) - Store reference to main table
- Implement public API methods
- Use EventManager for communication
2. Event Integration
// Trigger events for extensibility
this.table.eventManager.trigger('beforeAction', data);
// ... perform action ...
this.table.eventManager.trigger('afterAction', result);
3. CSS Isolation
- Use
tablix-prefix for all CSS classes - Support CSS custom properties for theming
- Provide both light and dark theme support
4. Configuration
- Provide sensible defaults
- Support both global and per-instance configuration
- Document all options
5. Memory Management
- Clean up event listeners in destroy methods
- Remove DOM elements when plugin is disabled
- Avoid memory leaks in long-running applications
Plugin Distribution
NPM Package Structure
my-tablix-plugin/
├── src/
│ ├── MyPluginManager.js
│ ├── MyPluginUI.js
│ └── styles/
│ └── my-plugin.css
├── dist/
│ ├── my-plugin.js
│ └── my-plugin.css
├── package.json
└── README.md
Usage Example
import Table from 'tablixjs';
import MyPlugin from 'my-tablix-plugin';
// Register plugin
Table.registerPlugin('myPlugin', MyPlugin);
// Use plugin
const table = new Table('#myTable', {
myPlugin: {
enabled: true,
// plugin options
}
});
This plugin architecture ensures TablixJS remains lightweight while allowing for rich functionality through optional plugins.