Examples
Case 4: Budget Management System with Policies

πŸ“Š 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

βœ… Approved
⏳ Pending
πŸ” Under Review

These status options are dynamically managed and used in the Status column above.

Implementation Guide

πŸ“„ View Source Code

Dropdown 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