Skip to content

Policy System

The Policy system controls how cells render values, parse user input, serialize data, and restrict operations. It is GridSheet’s primary extension point for custom cell behavior.

A Policy instance is:

  • Composable — built from one or more PolicyMixinType objects
  • Priority-based — the priority number determines which policy wins when multiple apply
  • Per-cell — each cell references a policy by its string key (set via cell.policy)

Policies are registered through useBook and referenced by key in cell definitions:

import { GridSheet, Policy, useBook, ThousandSeparatorPolicyMixin } from '@gridsheet/react-core';
function MySheet() {
const book = useBook({
policies: {
currency: new Policy({ mixins: [ThousandSeparatorPolicyMixin] }),
},
});
return (
<GridSheet
book={book}
initialCells={{
B: { policy: 'currency' },
'B1': { value: 1234567.89 },
}}
/>
);
}

Use useSpellbook from @gridsheet/react-core/spellbook to create a book with all extended formula functions pre-loaded:

import { GridSheet, Policy, ThousandSeparatorPolicyMixin } from '@gridsheet/react-core';
import { useSpellbook } from '@gridsheet/react-core/spellbook';
function MySheet() {
const book = useSpellbook({
policies: {
currency: new Policy({ mixins: [ThousandSeparatorPolicyMixin] }),
},
});
return (
<GridSheet
book={book}
initialCells={{
B: { policy: 'currency' },
'B1': { value: 1234567.89 },
}}
/>
);
}

Define a PolicyMixinType object and pass it to the Policy constructor:

import { Policy, PolicyMixinType, RenderProps, SelectProps, AutocompleteOption } from '@gridsheet/react-core';
const StatusPolicyMixin: PolicyMixinType = {
getSelectOptions(): AutocompleteOption[] {
return [
{ value: 'active', label: '🟢 Active' },
{ value: 'inactive', label: '🔴 Inactive' },
{ value: 'pending', label: '🟡 Pending' },
];
},
select({ next }: SelectProps) {
const allowed = ['active', 'inactive', 'pending'];
if (!allowed.includes(next?.value)) {
return { ...next, value: 'pending' };
}
return next;
},
renderString({ value }: RenderProps<string>) {
const map: Record<string, string> = {
active: '🟢 Active',
inactive: '🔴 Inactive',
pending: '🟡 Pending',
};
return map[value] ?? value;
},
};
const statusPolicy = new Policy({ mixins: [StatusPolicyMixin] });
import { Policy, ThousandSeparatorPolicyMixin } from '@gridsheet/react-core';
const currencyPolicy = new Policy({
mixins: [
ThousandSeparatorPolicyMixin,
{
renderColHeaderLabel: () => 'Amount ($)',
},
],
});
import { Policy, RenderProps } from '@gridsheet/react-core';
class UpperCasePolicy extends Policy {
override renderString({ value }: RenderProps<string>) {
return value?.toUpperCase() ?? '';
}
}
const upperCasePolicy = new UpperCasePolicy();
new Policy(props?: PolicyProps)
type PolicyProps = {
mixins?: PolicyMixinType[];
priority?: number; // default: 1
};
PropertyTypeDefaultDescription
mixinsPolicyMixinType[]undefinedMixin objects whose methods are merged into the policy
prioritynumber1Resolution priority when multiple policies apply
PropertyTypeDefaultDescription
datetimeFormatstring'YYYY-MM-DD HH:mm:ss'Format for datetime values
dateFormatstring'YYYY-MM-DD'Format for date-only values
timeFormatstring'HH:mm:ss'Format for time values
decimalPrecisionnumber15Significant digits for number rendering

These methods control what is displayed inside a cell. Return a React node or a primitive value.

MethodSignatureDescription
render(props: RenderProps) => anyOverride the entire render dispatch
renderCallback(rendered: any, props: RenderProps) => anyPost-process the rendered value
renderString(props: RenderProps<string>) => anyRender string values
renderNumber(props: RenderProps<number>) => anyRender number values
renderBool(props: RenderProps<boolean>) => anyRender boolean values
renderDate(props: RenderProps<Date>) => anyRender Date values
renderTime(props: RenderProps<Time>) => anyRender Time values
renderArray(props: RenderProps<any[]>) => anyRender array values
renderObject(props: RenderProps<any>) => anyRender object values
renderNull(props: RenderProps<null | undefined>) => anyRender null/undefined values
renderSheet(props: RenderProps<UserSheet>) => anyRender Sheet values
renderRowHeaderLabel(n: number) => string | nullCustom row header label
renderColHeaderLabel(n: number) => string | nullCustom column header label

RenderProps

type RenderProps<T = any> = {
value?: T;
cell?: CellType<T>;
sheet: Sheet;
point: PointType;
sync?: (sheet: UserSheet) => void;
};

These methods convert cell values to strings for clipboard and export.

MethodSignatureDescription
serialize(props: SerializeProps) => stringOverride entire serialization dispatch
serializeString(props: SerializeProps<string>) => stringSerialize string values
serializeNumber(props: SerializeProps<number>) => stringSerialize number values
serializeBool(props: SerializeProps<boolean>) => stringSerialize boolean values
serializeDate(props: SerializeProps<Date>) => stringSerialize Date values
serializeTime(props: SerializeProps<Time>) => stringSerialize Time values
serializeArray(props: SerializeProps<any[]>) => stringSerialize array values
serializeNull(props: SerializeProps<null | undefined>) => stringSerialize null/undefined values
serializeFormulaError(props: SerializeProps<FormulaError>) => stringSerialize formula errors
serializeError(props: SerializeProps<Error>) => stringSerialize generic errors
serializeForClipboard(props: SerializeForClipboardProps) => stringCustom clipboard serialization

SerializeProps

type SerializeProps<T = any> = {
value: T;
cell?: CellType<T>;
sheet: Sheet;
point: PointType;
};

These methods parse user input (strings) into typed cell values.

MethodSignatureDescription
deserialize(value: string, cell?: CellType) => CellTypeOverride entire deserialize dispatch
deserializeRaw(value: any, cell?: CellType) => CellTypeParse raw (non-string) input
deserializeCallback(parsed: any, cell?: CellType) => CellTypePost-process any deserialized value
deserializeFirst(value: string, cell?: CellType) => CellPatchType<any>Runs before built-in deserializers (highest priority)
deserializeNumber(value: string, cell?: CellType) => CellPatchType<number | undefined>Parse as number
deserializeBool(value: string, cell?: CellType) => CellPatchType<boolean | undefined>Parse as boolean
deserializeDate(value: string, cell?: CellType) => CellPatchType<Date | undefined>Parse as Date
deserializeTime(value: string, cell?: CellType) => CellPatchType<Time | undefined>Parse as Time
deserializeAny(value: string, cell?: CellType) => CellPatchType<string | undefined>Fallback — treat as string

Deserialization order (built-in):

  1. deserializeFirst (if defined — runs before all others)
  2. deserializeNumber
  3. deserializeTime
  4. deserializeDate
  5. deserializeBool
  6. deserializeAny

These methods convert cell values to scalars used by the formula engine.

MethodSignatureDescription
toScalar(props: ScalarProps) => ScalarOverride entire scalar dispatch
toScalarString(props: ScalarProps<string>) => ScalarConvert string to scalar
toScalarNumber(props: ScalarProps<number>) => ScalarConvert number to scalar
toScalarBool(props: ScalarProps<boolean>) => ScalarConvert boolean to scalar
toScalarDate(props: ScalarProps<Date>) => ScalarConvert Date to scalar
toScalarTime(props: ScalarProps<Time>) => ScalarConvert Time to scalar
toScalarArray(props: ScalarProps<any[]>) => ScalarConvert array to scalar
toScalarNull(props: ScalarProps<null | undefined>) => ScalarConvert null/undefined to scalar
type ScalarProps<T = any> = {
value?: T;
cell?: CellType<T>;
sheet: Sheet;
point: PointType;
};
type Scalar = string | number | boolean | null | undefined;

These methods control dropdown menus and value validation when users select from a list.

MethodSignatureDescription
getSelectOptions() => AutocompleteOption[]Return available dropdown options
select(props: SelectProps) => CellType | undefinedValidate/transform the selected value
getSelectFallback(props: SelectFallbackProps) => CellType | undefinedReturn a default cell when validation fails

AutocompleteOption

type AutocompleteOption = {
value: any;
label?: any; // React node displayed in dropdown
keywords?: string[]; // Extra search terms
tooltip?: ReactNode | FC<{ value?: any }>; // Tooltip shown on hover
};

SelectProps

type SelectProps = {
sheet: UserSheet;
point: PointType;
current?: CellType; // Cell value before the change
next?: CellType; // Cell value after the change
operation: OperationType;
};
import { Policy, PolicyMixinType, AutocompleteOption, SelectProps } from '@gridsheet/react-core';
const ColorPolicyMixin: PolicyMixinType = {
getSelectOptions(): AutocompleteOption[] {
return [
{ value: 'red', label: <span style={{ color: 'red' }}>Red</span> },
{ value: 'green', label: <span style={{ color: 'green' }}>Green</span> },
{ value: 'blue', label: <span style={{ color: 'blue' }}>Blue</span> },
];
},
select({ next, current }: SelectProps) {
const allowed = ['red', 'green', 'blue'];
if (allowed.includes(next?.value)) {
return { value: next!.value, style: { backgroundColor: next!.value } };
}
// Preserve current value if input is invalid
return current ?? { value: null };
},
};
const colorPolicy = new Policy({ mixins: [ColorPolicyMixin], priority: 2 });

GridSheet ships with several ready-to-use mixins:

Formats numbers with comma separators.

import { Policy, ThousandSeparatorPolicyMixin } from '@gridsheet/react-core';
const policy = new Policy({ mixins: [ThousandSeparatorPolicyMixin] });
// 1234567.89 → "1,234,567.89"

Same as above but uses a space as the thousands separator.

import { Policy, ThousandSpaceSeparatorPolicyMixin } from '@gridsheet/react-core';
const policy = new Policy({ mixins: [ThousandSpaceSeparatorPolicyMixin] });
// 1234567.89 → "1 234 567.89"

Renders boolean cells as interactive <input type="checkbox"> elements.

import { Policy, CheckboxPolicyMixin } from '@gridsheet/react-core';
const policy = new Policy({ mixins: [CheckboxPolicyMixin] });
// Registration and cell assignment
const book = useBook({
policies: {
checkbox: new Policy({ mixins: [CheckboxPolicyMixin] }),
},
});
const initialCells = {
A: { policy: 'checkbox' },
'A1': { value: true },
'A2': { value: false },
};

A minimal policy with priority: 0 that applies no customization. Used as the baseline default.

import { nonePolicy } from '@gridsheet/react-core';