nummo-sheet
v1.0.3
Published
Reusable Excel component for React applications
Readme
Nummo Spreadsheet
A reusable React spreadsheet component with Excel-like functionality, designed to be easily integrated into any React application, including Electron apps.
Features
- Excel-like grid with rows, columns, and cells
- Formula support via HyperFormula
- Cell formatting (bold, italic, alignment, colors)
- Copy/paste functionality
- Multiple sheets
- Import/export JSON data
- Electron compatibility
- Programmatic API for external control
Installation
npm install nummo-sheet
# or
yarn add nummo-sheetPeer Dependencies
This package has the following peer dependencies that need to be installed in your project:
npm install react react-dom tailwindcssBasic Usage
import { Spreadsheet } from 'nummo-sheet';
// Import the styles
import 'nummo-sheet/styles.css';
function App() {
return (
<div style={{ width: '100%', height: '600px' }}>
<Spreadsheet />
</div>
);
}Importing Data
You can import data from JSON using the jsonData prop:
import { Spreadsheet } from 'nummo-sheet';
import 'nummo-sheet/styles.css';
function App() {
const data = {
tables: [
{
table_name: 'Sheet 1',
cells: [
[
{ value: 'Name' },
{ value: 'Age' },
{ value: 'City' }
],
[
{ value: 'John' },
{ value: 30 },
{ value: 'New York' }
]
]
}
]
};
return (
<div style={{ width: '100%', height: '600px' }}>
<Spreadsheet jsonData={data} />
</div>
);
}Programmatic API
The spreadsheet component provides a programmatic API for external control. You can access this API using the onReady callback:
import { Spreadsheet } from 'nummo-sheet';
import { SpreadsheetAPI } from 'nummo-sheet/types';
import 'nummo-sheet/styles.css';
function App() {
const handleReady = (api) => {
// Store the API reference for later use
spreadsheetApi.current = api;
// Example: Get current data
const data = api.getData();
console.log('Current spreadsheet data:', data);
// Example: Set a cell value
api.setCellValue(0, 0, 'Hello from API');
};
const spreadsheetApi = useRef(null);
const handleButtonClick = () => {
// Use the API to interact with the spreadsheet
if (spreadsheetApi.current) {
spreadsheetApi.current.setCellValue(0, 1, 'Updated value');
}
};
return (
<div>
<button onClick={handleButtonClick}>Update Cell</button>
<div style={{ width: '100%', height: '600px' }}>
<Spreadsheet onReady={handleReady} />
</div>
</div>
);
}Available API Methods
| Method | Description |
|--------|-------------|
| getData() | Get the current spreadsheet data |
| setData(data) | Set spreadsheet data from external source |
| getSelection() | Get the current selection |
| setSelection(selection) | Set the current selection |
| getCellValue(row, col, sheetIndex?) | Get the value of a specific cell |
| setCellValue(row, col, value, sheetIndex?) | Set the value of a specific cell |
| addSheet(name) | Add a new sheet |
| switchSheet(index) | Switch to a specific sheet |
| exportToJson() | Export the current spreadsheet data to JSON format |
Electron Integration
When using this component in an Electron application, you'll need to handle browser-specific APIs differently. The spreadsheet component provides several abstractions and props to help with this.
Storage Abstraction
The module provides a StorageProvider interface that abstracts storage operations. This allows you to implement custom storage solutions for Electron, such as using the file system or IPC communication with the main process instead of localStorage.
// Types for storage abstraction
import type { StorageProvider } from 'nummo-sheet/types';
// Helper functions for creating storage providers
import { createMemoryStorageProvider, createLocalStorageProvider } from 'nummo-sheet';
// In-memory storage provider (useful for Electron)
const memoryStorage = createMemoryStorageProvider();
// Custom storage provider using Electron's main process
const createElectronStorageProvider = (): StorageProvider => {
return {
getItem: (key) => {
// Use IPC to get data from main process
return window.electron.invoke('get-storage-item', key);
},
setItem: (key, value) => {
// Use IPC to save data to main process
return window.electron.invoke('set-storage-item', key, value);
},
removeItem: (key) => {
// Use IPC to remove data in main process
return window.electron.invoke('remove-storage-item', key);
}
};
};Clipboard Abstraction
The module also provides a ClipboardProvider interface to abstract clipboard operations. This allows you to use Electron's clipboard API instead of the browser's navigator.clipboard.
// Types for clipboard abstraction
import type { ClipboardProvider } from 'nummo-sheet/types';
// Custom clipboard provider using Electron's clipboard API
const createElectronClipboardProvider = (): ClipboardProvider => {
return {
readText: () => {
// Use Electron's clipboard API
return Promise.resolve(require('electron').clipboard.readText());
},
writeText: (text) => {
// Use Electron's clipboard API
require('electron').clipboard.writeText(text);
return Promise.resolve();
}
};
};Complete Electron Integration Example
import { Spreadsheet } from 'nummo-spreadsheet';
import type { ClipboardProvider, StorageProvider } from 'nummo-spreadsheet/types';
import 'nummo-spreadsheet/dist/styles.css';
import { useState, useEffect, useRef } from 'react';
// Custom Electron storage provider
const createElectronStorageProvider = (): StorageProvider => {
return {
getItem: (key) => window.electron.invoke('get-storage-item', key),
setItem: (key, value) => window.electron.invoke('set-storage-item', key, value),
removeItem: (key) => window.electron.invoke('remove-storage-item', key)
};
};
// Custom Electron clipboard provider
const createElectronClipboardProvider = (): ClipboardProvider => {
const { clipboard } = window.require('electron');
return {
readText: () => Promise.resolve(clipboard.readText()),
writeText: (text) => {
clipboard.writeText(text);
return Promise.resolve();
}
};
};
function ElectronApp() {
// Create custom providers for Electron
const storageProvider = createElectronStorageProvider();
const clipboardProvider = createElectronClipboardProvider();
// Reference to the spreadsheet API
const spreadsheetApi = useRef(null);
// Example data from Electron main process
const [data, setData] = useState(null);
useEffect(() => {
// Listen for data from the main process
window.electron.receive('spreadsheet-data', (receivedData) => {
setData(receivedData);
});
// Listen for commands from the main process
window.electron.receive('spreadsheet-command', (command, ...args) => {
if (spreadsheetApi.current) {
// Execute commands from main process
switch (command) {
case 'export':
const data = spreadsheetApi.current.exportToJson();
window.electron.send('spreadsheet-exported', data);
break;
case 'set-cell':
const [row, col, value] = args;
spreadsheetApi.current.setCellValue(row, col, value);
break;
// Add more command handlers as needed
}
}
});
}, []);
const handleDataChange = (updatedData) => {
// Send updated data back to the main process
window.electron.send('update-spreadsheet-data', updatedData);
};
const handleReady = (api) => {
// Store API reference for programmatic control
spreadsheetApi.current = api;
// Notify main process that the spreadsheet is ready
window.electron.send('spreadsheet-ready');
};
return (
<div style={{ width: '100%', height: '600px' }}>
<Spreadsheet
jsonData={data}
onDataChange={handleDataChange}
onReady={handleReady}
storageProvider={storageProvider}
useSystemClipboard={true}
useFileSystem={true}
/>
</div>
);
}Main Process Implementation (Electron)
Here's an example of how to set up the main process to communicate with the spreadsheet component:
// In main.js or preload.js
const { contextBridge, ipcRenderer, ipcMain } = require('electron');
// Preload script for exposing safe APIs to renderer
contextBridge.exposeInMainWorld('electron', {
// IPC communication
send: (channel, ...args) => ipcRenderer.send(channel, ...args),
receive: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
// File system operations
saveFile: (options) => ipcRenderer.invoke('save-file', options),
openFile: (options) => ipcRenderer.invoke('open-file', options)
});
// In the main process
let storageData = {}; // In-memory storage for the main process
// Handle storage operations
ipcMain.handle('get-storage-item', (event, key) => {
return storageData[key] || null;
});
ipcMain.handle('set-storage-item', (event, key, value) => {
storageData[key] = value;
return true;
});
ipcMain.handle('remove-storage-item', (event, key) => {
delete storageData[key];
return true;
});
// Handle file operations
ipcMain.handle('save-file', async (event, { defaultPath, filters, data }) => {
const { dialog } = require('electron');
const { filePath } = await dialog.showSaveDialog({
defaultPath,
filters
});
if (filePath) {
const fs = require('fs');
fs.writeFileSync(filePath, data);
return { success: true, filePath };
}
return { success: false };
});
ipcMain.handle('open-file', async (event, { filters }) => {
const { dialog } = require('electron');
const { filePaths } = await dialog.showOpenDialog({
properties: ['openFile'],
filters
});
if (filePaths && filePaths.length > 0) {
const fs = require('fs');
const data = fs.readFileSync(filePaths[0], 'utf8');
return { success: true, data, filePath: filePaths[0] };
}
return { success: false };
});Electron-specific Props
| Prop | Type | Description |
|------|------|-------------|
| storageProvider | StorageProvider | Custom storage implementation for persistence. Use this to implement Electron-specific storage. |
| useSystemClipboard | boolean | When true, the spreadsheet will attempt to use the system clipboard. In Electron, you should provide a custom clipboard provider through the hooks. |
| useFileSystem | boolean | When true, file operations will be delegated to the host application. In Electron, you should implement file system operations using the Electron API. |
| readOnly | boolean | Make the spreadsheet read-only. Useful for view-only scenarios. |
Handling File Operations in Electron
For file operations like import/export, you'll need to handle these through Electron's dialog and fs modules:
// In your Electron renderer process
const handleExport = () => {
if (spreadsheetApi.current) {
const data = spreadsheetApi.current.exportToJson();
window.electron.saveFile({
defaultPath: 'spreadsheet-data.json',
filters: [{ name: 'JSON Files', extensions: ['json'] }],
data: JSON.stringify(data, null, 2)
});
}
};
const handleImport = async () => {
const result = await window.electron.openFile({
filters: [{ name: 'JSON Files', extensions: ['json'] }]
});
if (result.success) {
try {
const jsonData = JSON.parse(result.data);
// Set the data to the spreadsheet
if (spreadsheetApi.current) {
spreadsheetApi.current.setData(jsonData);
}
} catch (error) {
console.error('Failed to parse JSON:', error);
}
}
};TypeScript Support
This package includes TypeScript definitions. You can import types directly:
import { Spreadsheet } from 'nummo-spreadsheet';
import type { SpreadsheetAPI, SpreadsheetProps } from 'nummo-spreadsheet/types';{ files: ['**/*.{ts,tsx}'], extends: [ // Other configs... // Enable lint rules for React reactX.configs['recommended-typescript'], // Enable lint rules for React DOM reactDom.configs.recommended, ], languageOptions: { parserOptions: { project: ['./tsconfig.node.json', './tsconfig.app.json'], tsconfigRootDir: import.meta.dirname, }, // other options... }, }, ])
