Skip to content

Input masks & validation

Problem: phone numbers, IBANs, postcodes and currency fields that guide the user as they type — and cell values that are checked against a schema before they’re accepted.

The Editing plugin’s built-in editors are deliberately plain <input> / <select> elements. Masking and schema validation are solved problems with mature, well-tested libraries; a grid-shaped subset of one would be worse than the real thing and would cost every consumer bytes. The seam is column.editor — it hands you a cell and takes back any element — plus the cell-commit event and the plugin’s own setInvalid() API, so third-party validation lights up the standard invalid-cell styling.

LibraryApprox. size (min+gz)Purpose
IMask~15 kBAttaches a mask to a normal <input>
Zod~14 kBSchema validation (or Valibot ~2 kB, Yup ~12 kB)

IMask attaches to a normal <input>, so the editor keeps the grid’s own sizing and focus behaviour. Commit mask.unmaskedValue to keep the stored value clean while the user sees the mask.

import IMask from 'imask';
{
field: 'phone',
header: 'Phone',
editable: true,
editor: (ctx) => {
const input = document.createElement('input');
const mask = IMask(input, { mask: '+{47} 000 00 000' });
mask.unmaskedValue = String(ctx.value ?? '');
// One-shot: Escape fires cancel, then the removed input blurs and fires again.
let done = false;
const finish = (commit: boolean) => {
if (done) return;
done = true;
const value = mask.unmaskedValue;
mask.destroy();
commit ? ctx.commit(value) : ctx.cancel();
};
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') finish(true);
if (e.key === 'Escape') finish(false);
});
input.addEventListener('blur', () => finish(true));
return input;
},
}

Validation stays outside the editor. Check the committed value in cell-commit and mark failures with setInvalid(), so they pick up the standard invalid-cell styling.

import { z } from 'zod';
const schema: Record<string, z.ZodTypeAny> = {
email: z.string().email('Not a valid email address'),
age: z.number().int().min(18, 'Must be 18 or older'),
};
const editing = grid.getPluginByName('editing');
grid.addEventListener('cell-commit', (e) => {
const { rowId, field, value } = e.detail;
const rule = schema[field];
if (!rule) return;
const result = rule.safeParse(value);
if (result.success) {
editing?.clearInvalid(rowId, field);
} else {
editing?.setInvalid(rowId, field, result.error.issues[0].message);
// Use e.preventDefault() instead to reject the value outright rather than
// storing it and flagging the cell.
}
});
  • Always call mask.destroy(), exactly once. IMask attaches its own listeners to the input. The grid removes the editor element after a commit or cancel, so tear the mask down on both paths — but guard against re-entry: pressing Escape calls finish once, and the resulting blur calls it again. ctx.commit() / ctx.cancel() are idempotent (the grid tracks an internal finalized flag), so a stray commit is harmless — mask.destroy() is not.
  • Decide what you store. mask.value is what the user sees; mask.unmaskedValue is the raw digits. Store the raw value and add a format function for display, or your exports and clipboard output will carry the mask characters.
  • setInvalid() does not reject the value. It stores the value and flags the cell, which is usually what you want for a form-style grid. To refuse the value outright, call e.preventDefault() in cell-commit — see Cell Validation.
  • Paste bypasses the editor, not the event. Clipboard paste writes through the same commit pipeline, so cell-commit validation still fires — but the mask never runs. Validate the shape, don’t assume the mask enforced it.
  • rowId requires getRowId. setInvalid() is keyed by row id; without a configured getRowId there is nothing stable to key on.
  • Overlay editors need registering. If your third-party editor renders a dropdown or datepicker outside the cell, call grid.registerExternalFocusContainer(overlay) so the grid doesn’t close the editor when the overlay takes focus.
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://toolboxjs.com/llms-full.txt