π Budget Management System with Policies
This demo showcases advanced Policy functionality for data validation, business rules, and real-time event monitoring.
It demonstrates a comprehensive budget management system with multiple policy types including department selection, status validation, budget constraints, and comprehensive event handling.
The status options can be dynamically managed using a modern multi-select interface, and all spreadsheet operations are monitored through event handlers.
π Budget Overview
βοΈ Available Status Options
These status options are dynamically managed and used in the Status column above.
Implementation Guide
π View Source CodeDropdown Selection with getSelectOptions
Implementing getSelectOptions on a PolicyMixinType enables a dropdown (autocomplete) UI for that cell. The returned array of AutocompleteOption objects drives the option list.
Each option can carry a keywords array for fuzzy/multi-keyword matching, so searching "dev" or "Engineering" both resolve to the same department.
const DEPARTMENT_OPTIONS: AutocompleteOption[] = [
{ value: 'Engineering', label: 'π§ Engineering', keywords: ['Engineering', 'dev', 'development'] },
{ value: 'Marketing', label: 'π’ Marketing', keywords: ['Marketing', 'promotion'] },
// ...
];
const DepartmentPolicy: PolicyMixinType = {
getSelectOptions: () => DEPARTMENT_OPTIONS,
select: ({ next }) => {
const valid = DEPARTMENT_OPTIONS.find((o) => o.value === next?.value);
return valid ? next : { ...next, value: 'Engineering' }; // fall back to default
},
};The select hook runs on every commit and lets you sanitize or reject a value before it is written to the cell.
Input Validation with the select Hook
The select hook on PolicyMixinType intercepts the value the user is about to write. Return a modified next object to correct out-of-range inputs automatically.
const BudgetPolicy: PolicyMixinType = {
select: ({ next }) => {
const value = Number(next?.value);
if (isNaN(value) || value < 0) return { ...next, value: 0 };
if (value > 1_000_000) return { ...next, value: 1_000_000 };
return next;
},
};Combining BudgetPolicy with ThousandSeparatorPolicyMixin in a single policy applies both range clamping and thousands-separator display.
Dynamic Policy Options via useRef
When a policy's options must update in real time (e.g., a tag list the user can edit), store the current options in a ref instead of closing over state. This avoids recreating the policy object on every render.
const statusOptionsRef = React.useRef<[string, string][]>([]);
React.useEffect(() => {
statusOptionsRef.current = tags.map((tag) => [tag.value, tag.label]);
}, [tags]);
const StatusPolicy = React.useMemo(
() => ({
getSelectOptions: () =>
statusOptionsRef.current.map(([value, label]) => ({ value, label })),
select: ({ next }) => {
const valid = statusOptionsRef.current.map(([v]) => v).includes(next?.value);
return valid ? next : { ...next, value: 'Pending' };
},
}),
[tags], // policy object is recreated only when tags change
);Because getSelectOptions reads from the ref at call time, even an old policy instance always sees the latest options.
CheckboxPolicyMixin for Boolean Cells
The built-in CheckboxPolicyMixin renders boolean cell values as interactive checkboxes.
const book = useSpellbook({
policies: {
checkbox: new Policy({ mixins: [CheckboxPolicyMixin] }),
},
});
// In cells:
F: { policy: 'checkbox', alignItems: 'center', justifyContent: 'center' }Cell values of true / false are toggled by clicking the rendered checkbox.
Border Styling with makeBorder
makeBorder generates individual CSS border properties (borderTop, borderRight, etc.) without touching the shorthand border property. This avoids accidentally overriding other border declarations.
import { makeBorder } from '@gridsheet/react-core';
// Uniform border on all sides
default: { style: { ...makeBorder({ all: '1px solid #000000' }) } }
// Double border only on the bottom (used to separate the Total row)
'A6:F6': { style: { ...makeBorder({ bottom: '4px double #000000' }) } }The all shorthand sets all four sides at once, while named sides (top, right, bottom, left) let you style individual edges.
Event Monitoring with useSpellbook Callbacks
useSpellbook accepts a rich set of event callbacks that are called after specific grid operations.
const book = useSpellbook({
onSave: ({ sheet, points }) => log(`Saved at ${points.length} position(s)`),
onChange: ({ sheet, points }) => log(`Changed at ${points.length} position(s)`),
onSelect: ({ sheet, points }) => log(`Selected ${points.length} cell(s)`),
onRemoveRows: ({ sheet, ys }) => log(`Removed rows: ${ys.join(', ')}`),
onInsertRows: ({ sheet, y, numRows }) => log(`Inserted ${numRows} row(s) at ${y}`),
onKeyUp: ({ e }) => log(`Key: ${e.key}`),
onInit: ({ sheet }) => log(`Initialized: ${sheet.name}`),
});These callbacks are ideal for audit logs, analytics, or syncing changes to an external backend.
Connecting CreatableSelect to a Dynamic Policy
To let users add or remove status options at runtime, use CreatableSelect from react-select/creatable to manage a tag list in React state, then feed that state into the policy via a ref (see above).
<CreatableSelect
options={tags}
value={tags}
onChange={handleSelectChange}
isMulti
isClearable
onCreateOption={(inputValue) => {
setTags((prev) => [...prev, { value: inputValue, label: `π ${inputValue}` }]);
}}
/>Changes to tags propagate to statusOptionsRef via useEffect, so the policy's getSelectOptions immediately returns the updated list without remounting the grid.
Keeping Summary Rows Fixed with sortFixed and filterFixed
Row-level options sortFixed and filterFixed prevent a specific row from being reordered during sort or hidden during filter operations. This is essential for summary rows like totals that must always remain visible.
These flags are set on the row's header cell β the cell at column 0 for the target row. The address format is '0<row>' (e.g., '07' for row 7).
buildInitialCells({
cells: {
// ... data cells ...
'07': { sortFixed: true, filterFixed: true },
},
})sortFixed: The row stays in its original position when any column is sorted. Other rows move around it.filterFixed: The row always remains visible regardless of active filter conditions.
Rows with filterFixed show a βΉ prefix and rows with sortFixed show a βΊ suffix in their row header, applied automatically via CSS.
π Event Handling and Monitoring
This budget management system includes comprehensive event handling to monitor all spreadsheet operations in real-time:
π Real-time Event Monitoring
- Data Events: Monitor when budget data is saved or changed
- Selection Events: Track cell selection changes for user interaction analysis
- Structure Events: Monitor row and column insertions/removals for audit trails
- Keyboard Events: Track keyboard interactions for user experience analysis
- Initialization Events: Monitor table initialization for system health
π Event Logging System
The system includes a real-time event log that displays:
- Timestamp: Precise timing of each event
- Event Type: Clear identification of the operation type
- Affected Data: Information about what data was modified
- User Actions: Tracking of user interactions and changes
π― Event Handler Implementation
const hub = useHub({
// Data monitoring events
onSave: ({ table, points }) => {
addEventLog(`Budget data saved at ${Array.isArray(points) ? points.length : 1} position(s)`);
},
onChange: ({ table, points }) => {
addEventLog(`Budget data changed at ${Array.isArray(points) ? points.length : 1} position(s)`);
},
// Structure monitoring events
onRemoveRows: ({ table, ys }) => {
addEventLog(`Removed ${ys.length} row(s): ${ys.join(', ')}`);
},
onInsertRows: ({ table, y, numRows }) => {
addEventLog(`Inserted ${numRows} row(s) at position ${y}`);
},
// User interaction events
onSelect: ({ table, points }) => {
addEventLog(`Selected ${Array.isArray(points) ? points.length : 1} cell(s)`);
},
onKeyUp: ({ e, points }) => {
addEventLog(`Key pressed: ${e.key}`);
},
});π Event-Driven Architecture Benefits
- Audit Trails: Complete tracking of all budget modifications
- User Analytics: Understanding of user interaction patterns
- Debugging: Real-time monitoring for troubleshooting
- Compliance: Meeting regulatory requirements for data tracking
- Performance Monitoring: Tracking system performance and usage patterns