@sciphergfx/json-to-table
Version:
UI-agnostic React components for JSON to Table and JSON to Form Fields conversion with support for Chakra UI, Tailwind CSS, and shadcn/ui
254 lines (223 loc) • 7.54 kB
JSX
import React, { useState, useEffect } from 'react';
import { getUIComponents, getUIClasses } from '../utils/uiAdapters';
import { getDisplayName } from '../utils/jsonUtils';
/**
* JsonToTable Component - UI Library Agnostic
* @param {Object} props - Component props
* @param {string} props.uiLibrary - UI library to use ("chakra", "tailwind", "shadcn")
* @param {Function} props.onSave - Callback when save is triggered (nestedData, flatData) => void
* @param {Function} props.onCancel - Callback when cancel is triggered () => void
* @param {Function} props.onFieldChange - Callback when field changes (key, value, fullData) => void
* @param {string} props.saveButtonText - Text for save button
* @param {string} props.cancelButtonText - Text for cancel button
* @param {string} props.initialJson - Initial JSON string
* @param {Object} props.customStyles - Custom styles object
* @param {boolean} props.showControls - Whether to show save/cancel buttons
*/
const JsonToTable = ({
uiLibrary = 'chakra',
onSave,
onCancel,
onFieldChange,
saveButtonText = 'Save Changes',
cancelButtonText = 'Reset Form',
initialJson = '',
customStyles = {},
showControls = true,
...props
}) => {
const [jsonInput, setJsonInput] = useState(initialJson);
const [tableData, setTableData] = useState(null);
const [error, setError] = useState('');
const [editableData, setEditableData] = useState(null);
const UI = getUIComponents(uiLibrary);
useEffect(() => {
if (initialJson) {
setJsonInput(initialJson);
parseJson(initialJson);
} else if (initialJson === '') {
// Clear everything when initialJson is explicitly set to empty string
setJsonInput('');
setTableData(null);
setEditableData(null);
setError('');
}
}, [initialJson]); // parseJson is stable, no need to include
const parseJson = (jsonString = jsonInput) => {
setError('');
setTableData(null);
if (!jsonString.trim()) {
setError('Please enter some JSON data');
return;
}
try {
const parsed = JSON.parse(jsonString);
if (Array.isArray(parsed)) {
if (parsed.length === 0) {
setError('The JSON array is empty');
return;
}
setTableData(parsed);
setEditableData([...parsed]);
} else if (typeof parsed === 'object' && parsed !== null) {
const arrayData = [parsed];
setTableData(arrayData);
setEditableData([...arrayData]);
} else {
setError('JSON must be an object or array of objects');
return;
}
} catch (err) {
setError(`Invalid JSON: ${err.message}`);
}
};
const handleCellEdit = (rowIndex, key, value) => {
const newData = [...editableData];
newData[rowIndex][key] = value;
setEditableData(newData);
if (onFieldChange) {
onFieldChange(key, value, newData);
}
};
const handleSave = () => {
if (onSave && editableData) {
const flatData = editableData.map(row => ({ ...row }));
onSave(editableData, flatData);
}
};
const handleCancel = () => {
if (tableData) {
setEditableData([...tableData]);
}
if (onCancel) {
onCancel();
}
};
const getColumns = () => {
if (!tableData || tableData.length === 0) return [];
const allKeys = new Set();
tableData.forEach(row => {
Object.keys(row).forEach(key => allKeys.add(key));
});
return Array.from(allKeys);
};
const renderTable = () => {
if (!editableData || editableData.length === 0) return null;
const columns = getColumns();
return (
<UI.Box
className={getUIClasses(uiLibrary, 'Box')}
style={{ overflowX: 'auto', ...customStyles.tableContainer }}
>
<UI.Table
className={getUIClasses(uiLibrary, 'Table')}
style={customStyles.table}
>
<UI.Thead className={getUIClasses(uiLibrary, 'Thead')}>
<UI.Tr className={getUIClasses(uiLibrary, 'Tr')}>
{columns.map(column => (
<UI.Th
key={column}
className={getUIClasses(uiLibrary, 'Th')}
style={{ textAlign: 'center', ...customStyles.th }}
>
{getDisplayName(column)}
</UI.Th>
))}
</UI.Tr>
</UI.Thead>
<UI.Tbody className={getUIClasses(uiLibrary, 'Tbody')}>
{editableData.map((row, rowIndex) => (
<UI.Tr
key={rowIndex}
className={getUIClasses(uiLibrary, 'Tr')}
>
{columns.map(column => (
<UI.Td
key={column}
className={getUIClasses(uiLibrary, 'Td')}
style={customStyles.td}
>
<UI.Input
type="text"
value={row[column] || ''}
onChange={(e) => handleCellEdit(rowIndex, column, e.target.value)}
className={getUIClasses(uiLibrary, 'Input')}
style={{
border: 'none',
background: 'transparent',
...customStyles.input
}}
/>
</UI.Td>
))}
</UI.Tr>
))}
</UI.Tbody>
</UI.Table>
</UI.Box>
);
};
return (
<UI.Container
className={getUIClasses(uiLibrary, 'Container')}
style={customStyles.container}
{...props}
>
<UI.VStack
className={getUIClasses(uiLibrary, 'VStack')}
style={{ gap: '1rem', ...customStyles.stack }}
>
<UI.Box
className={getUIClasses(uiLibrary, 'Box')}
style={{ width: '100%', ...customStyles.inputContainer }}
>
<UI.Textarea
value={jsonInput}
onChange={(e) => setJsonInput(e.target.value)}
placeholder="Paste your JSON data here..."
className={getUIClasses(uiLibrary, 'Textarea')}
style={{
width: '100%',
minHeight: '150px',
fontFamily: 'monospace',
...customStyles.textarea
}}
/>
</UI.Box>
{error && (
<UI.Alert
className={getUIClasses(uiLibrary, 'Alert', 'error')}
style={customStyles.alert}
>
<UI.Text style={customStyles.errorText}>{error}</UI.Text>
</UI.Alert>
)}
{renderTable()}
{showControls && editableData && (
<UI.HStack
gap="2"
className={getUIClasses(uiLibrary, 'HStack')}
style={{ gap: '0.5rem', marginTop: '1rem', ...customStyles.controlButtons }}
>
<UI.Button
onClick={handleSave}
className={getUIClasses(uiLibrary, 'Button', 'default')}
style={customStyles.saveButton}
>
{saveButtonText}
</UI.Button>
<UI.Button
onClick={handleCancel}
className={getUIClasses(uiLibrary, 'Button', 'secondary')}
style={customStyles.cancelButton}
>
{cancelButtonText}
</UI.Button>
</UI.HStack>
)}
</UI.VStack>
</UI.Container>
);
};
export default JsonToTable;