Numbro
This tutorial shows you how to create a custom numeric cell type using the Numbro library for locale-aware number formatting.
/* file: app.component.ts */import { Component } from '@angular/core';import { GridSettings, HotTableModule } from '@handsontable/angular-wrapper';import numbro from 'numbro';// numbro ships no type declarations for its bundled language pack.// @ts-expect-error -- untyped moduleimport languages from 'numbro/dist/languages.min.js';import { rendererFactory, getRenderer } from 'handsontable/renderers';import { getEditor } from 'handsontable/editors';import { getValidator } from 'handsontable/validators';import { registerCellType } from 'handsontable/cellTypes';import { registerAllModules } from 'handsontable/registry';
// Register the modules here, not in app.config.ts: this file reaches for the built-in// numeric editor and validator while its own module body runs, which is before any// later import has been evaluated.registerAllModules();
Object.values(languages).forEach((language: any) => numbro.registerLanguage(language));
function isNumeric(value: any): boolean { const type = typeof value;
if (type === 'number') { return !isNaN(value) && isFinite(value); } else if (type === 'string') { if (value.length === 0) { return false; } else if (value.length === 1) { return /\d/.test(value); }
const delimiter = Array.from(new Set(['.'])) .map((d) => `\\${d}`) .join('|');
return new RegExp( `^[+-]?(((${delimiter})?\\d+((${delimiter})\\d+)?(e[+-]?\\d+)?)|(0x[a-f\\d]+))$`, 'i' ).test(value.trim()); } else if (type === 'object') { return !!value && typeof value.valueOf() === 'number' && !(value instanceof Date); }
return false;}
const cellTypeDefinition = { CELL_TYPE: 'numbro', renderer: rendererFactory(({ hotInstance, td, row, col, prop, value, cellProperties }: any) => { if (isNumeric(value)) { let classArr: string[] = [];
if (Array.isArray(cellProperties.className)) { classArr = cellProperties.className; } else { const className = cellProperties.className ?? '';
if (className.length) { classArr = className.split(' '); } }
const numericFormat = cellProperties.numericFormat; const cellCulture = (numericFormat && numericFormat.culture) || 'en-US'; const cellFormatPattern = numericFormat && numericFormat.pattern;
if (cellCulture && !numbro.languages()[cellCulture]) { const shortTag = cellCulture.replace('-', ''); const langData = (numbro as any).allLanguages ? (numbro as any).allLanguages[cellCulture] : (numbro as any)[shortTag];
if (langData) { numbro.registerLanguage(langData); } }
numbro.setLanguage(cellCulture); value = numbro(value).format(cellFormatPattern ?? '0');
if ( classArr.indexOf('htLeft') < 0 && classArr.indexOf('htCenter') < 0 && classArr.indexOf('htRight') < 0 && classArr.indexOf('htJustify') < 0 ) { classArr.push('htRight'); }
if (classArr.indexOf('htNumeric') < 0) { classArr.push('htNumeric'); }
cellProperties.className = classArr.join(' '); td.dir = 'ltr'; }
getRenderer('text')(hotInstance, td, row, col, prop, value, cellProperties); }), validator: getValidator('numeric'), editor: getEditor('numeric'),};
registerCellType('numbro', cellTypeDefinition);
@Component({ standalone: true, imports: [HotTableModule], selector: 'example1-numbro', template: `<div><hot-table [data]="data" [settings]="gridSettings"></hot-table></div>`,})export class AppComponent { readonly data = [ { itemName: 'Lunar Core', category: 'Lander', leadEngineer: 'Ellen Ripley', quantity: 2, cost: 350000, }, { itemName: 'Zero Thrusters', category: 'Propulsion', leadEngineer: 'Sam Bell', quantity: 0, cost: 450000, }, { itemName: 'EVA Suits', category: 'Equipment', leadEngineer: 'Alex Rogan', quantity: 50, cost: 150000, }, { itemName: 'Solar Panels', category: 'Energy', leadEngineer: 'Dave Bowman', quantity: 10, cost: 75000, }, { itemName: 'Comm Array', category: 'Communication', leadEngineer: 'Louise Banks', quantity: 0, cost: 125000, }, { itemName: 'Habitat Dome', category: 'Shelter', leadEngineer: 'Dr. Ryan Stone', quantity: 3, cost: 1000000, }, ];
readonly gridSettings: GridSettings = { colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Quantity', 'Cost'], autoRowSize: true, rowHeaders: true, height: 'auto', width: '100%', autoWrapRow: true, headerClassName: 'htLeft', columns: [ { data: 'itemName', type: 'text', width: 130 }, { data: 'category', type: 'text', width: 120 }, { data: 'leadEngineer', type: 'text', width: 150 }, { data: 'quantity', type: 'numbro' as any, width: 150, className: 'htRight', numericFormat: { pattern: '0,0', culture: 'en-US', }, } as any, { data: 'cost', type: 'numbro' as any, width: 120, className: 'htRight', numericFormat: { pattern: '$0,0.00', culture: 'en-US', }, } as any, ], };}/* end-file */
/* file: app.config.ts */import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';import { registerAllModules } from 'handsontable/registry';import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
registerAllModules();
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig, }, ],};/* end-file */<div> <example1-numbro></example1-numbro></div>Overview
This guide shows how to create a custom numbro cell type using the Numbro library. Users can format numbers using the Numbro API.
Difficulty: Beginner
Time: ~15 minutes
Libraries: numbro
What You’ll Build
A cell that:
- Displays numbers with locale-aware formatting via Numbro (e.g.,
$350,000.00) - Accepts
numericFormatoptions for per-column formatting customization - Validates input using Handsontable’s built-in numeric validator
- Automatically right-aligns numeric values
Prerequisites
npm install numbroImport Dependencies
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { rendererFactory, getRenderer } from 'handsontable/renderers';import { getEditor } from 'handsontable/editors';import { getValidator } from 'handsontable/validators';import { registerCellType } from 'handsontable/cellTypes';import numbro from 'numbro';// numbro ships no type declarations for its bundled language pack.// @ts-expect-error -- untyped moduleimport languages from 'numbro/dist/languages.min.js';registerAllModules();Object.values(languages).forEach((language) => numbro.registerLanguage(language));Why this matters:
numbrohandles locale-aware number formatting (currencies, decimals, thousands separators)rendererFactorycreates a custom renderer that formats values with Numbro before displaying- Registering all Numbro languages upfront enables any
cultureto be used innumericFormat - Numbro’s bundled language pack ships no TypeScript declarations, so the import needs
@ts-expect-errorin a strict TypeScript project
Create the Numeric Helper
This helper determines whether a value should be treated as a number:
function isNumeric(value) {const type = typeof value;if (type === 'number') {return !isNaN(value) && isFinite(value);} else if (type === 'string') {if (value.length === 0) return false;if (value.length === 1) return /\d/.test(value);const delimiter = Array.from(new Set(['.'])).map(d => `\\${d}`).join('|');return new RegExp(`^[+-]?(((${delimiter})?\\d+((${delimiter})\\d+)?(e[+-]?\\d+)?)|(0x[a-f\\d]+))$`, 'i').test(value.trim());} else if (type === 'object') {return !!value && typeof value.valueOf() === 'number' && !(value instanceof Date);}return false;}Create the Renderer
The renderer formats numeric values using Numbro and delegates to the built-in
textrenderer:renderer: rendererFactory(({ hotInstance, td, row, col, prop, value, cellProperties }) => {if (isNumeric(value)) {const numericFormat = cellProperties.numericFormat;const cellCulture = numericFormat && numericFormat.culture || 'en-US';const cellFormatPattern = numericFormat && numericFormat.pattern;numbro.setLanguage(cellCulture);value = numbro(value).format(cellFormatPattern ?? '0');// Auto-apply htRight alignment for numeric cellstd.dir = 'ltr';}getRenderer('text')(hotInstance, td, row, col, prop, value, cellProperties);})What’s happening:
- Reads
numericFormat.cultureandnumericFormat.patternfrom cell properties - Formats the raw number using
numbro(value).format(pattern) - Auto-applies
htRightalignment unless another alignment class is set - Sets
td.dir = 'ltr'for correct display in RTL layouts - Delegates to the
textrenderer for final DOM output
- Reads
Complete Cell Type Definition
Put all the pieces together and register the cell type:
const cellTypeDefinition = {renderer: rendererFactory(({ hotInstance, td, row, col, prop, value, cellProperties }) => {// ... renderer code from Step 3 (see full example above)}),validator: getValidator('numeric'),editor: getEditor('numeric'),};registerCellType('numbro', cellTypeDefinition);What’s happening:
- renderer: Formats numbers with Numbro and renders as right-aligned text
- validator: Uses the built-in numeric validator to reject non-numeric input
- editor: Uses the built-in numeric editor for input
- registerCellType: Registers the
numbrocell type for use in column config
Use in Handsontable
registerCellType('numbro', cellTypeDefinition);const hotOptions: Handsontable.GridSettings = {data,colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Quantity', 'Cost'],autoRowSize: true,rowHeaders: true,height: 'auto',width: '100%',autoWrapRow: true,headerClassName: 'htLeft',columns: [{ data: 'itemName', type: 'text', width: 130 },{ data: 'category', type: 'text', width: 120 },{ data: 'leadEngineer', type: 'text', width: 150 },{data: 'quantity',type: 'numbro',width: 150,className: 'htRight',numericFormat: {pattern: '0,0',culture: 'en-US',},},{data: 'cost',type: 'numbro',width: 120,className: 'htRight',numericFormat: {pattern: '$0,0.00',culture: 'en-US',},},],licenseKey: 'non-commercial-and-evaluation',};const hot = new Handsontable(container, hotOptions);Key configuration:
type: 'numbro'- uses the custom cell type on Quantity and Cost columnsnumericFormat.pattern- the Numbro format string (e.g.,'$0,0.00'for currency,'0,0'for integers)numericFormat.culture- the locale for formatting (e.g.,'en-US','de-DE')headerClassName: 'htLeft'- left-aligns all column headers
How It Works - Complete Flow
- Initial Render: Cell displays the raw number formatted by Numbro (e.g.,
350000becomes$350,000.00) - User clicks cell: The built-in numeric editor opens for editing
- User enters number: Input is validated against the numeric validator
- Validation: Non-numeric values are rejected; valid numbers are accepted
- Save: The value is stored as a raw number and re-rendered with Numbro formatting
What you learned
You created a custom Numbro-based numeric cell type in Handsontable. You used rendererFactory to format numbers with Numbro before display, registered all Numbro language packs for locale support, and composed the cell type from a custom renderer with the built-in numeric validator and editor.
Next steps
- Moment.js date - A custom cell type using another third-party library for date formatting.
- Moment.js time - A custom cell type using Moment.js for time validation.
- Pikaday - A date picker cell type using Pikaday and Moment.js.