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.
What you need
Section titled “What you need”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.
| Library | Approx. size (min+gz) | Purpose |
|---|---|---|
| IMask | ~15 kB | Attaches a mask to a normal <input> |
| Zod | ~14 kB | Schema validation (or Valibot ~2 kB, Yup ~12 kB) |
Masked editor
Section titled “Masked editor”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; },}Schema validation
Section titled “Schema validation”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. }});Caveats
Section titled “Caveats”- 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 callsfinishonce, 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.valueis what the user sees;mask.unmaskedValueis the raw digits. Store the raw value and add aformatfunction 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, calle.preventDefault()incell-commit— see Cell Validation.- Paste bypasses the editor, not the event. Clipboard paste writes through the same commit pipeline, so
cell-commitvalidation still fires — but the mask never runs. Validate the shape, don’t assume the mask enforced it. rowIdrequiresgetRowId.setInvalid()is keyed by row id; without a configuredgetRowIdthere 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.
See also
Section titled “See also”- Custom editors — the seam this recipe plugs into
- Cell validation — the built-in invalid-cell state and its CSS properties
- Type-level defaults — apply one custom editor to every column of a type