Skip to content

Numbro

This tutorial shows you how to create a custom numeric cell type using the Numbro library for locale-aware number formatting.

TypeScript
/* 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 module
import 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 */
HTML
<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 numericFormat options for per-column formatting customization
  • Validates input using Handsontable’s built-in numeric validator
  • Automatically right-aligns numeric values

Prerequisites

Terminal window
npm install numbro
  1. Import 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 module
    import languages from 'numbro/dist/languages.min.js';
    registerAllModules();
    Object.values(languages).forEach((language) => numbro.registerLanguage(language));

    Why this matters:

    • numbro handles locale-aware number formatting (currencies, decimals, thousands separators)
    • rendererFactory creates a custom renderer that formats values with Numbro before displaying
    • Registering all Numbro languages upfront enables any culture to be used in numericFormat
    • Numbro’s bundled language pack ships no TypeScript declarations, so the import needs @ts-expect-error in a strict TypeScript project
  2. 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;
    }
  3. Create the Renderer

    The renderer formats numeric values using Numbro and delegates to the built-in text renderer:

    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 cells
    td.dir = 'ltr';
    }
    getRenderer('text')(hotInstance, td, row, col, prop, value, cellProperties);
    })

    What’s happening:

    • Reads numericFormat.culture and numericFormat.pattern from cell properties
    • Formats the raw number using numbro(value).format(pattern)
    • Auto-applies htRight alignment unless another alignment class is set
    • Sets td.dir = 'ltr' for correct display in RTL layouts
    • Delegates to the text renderer for final DOM output
  4. 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 numbro cell type for use in column config
  5. 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 columns
    • numericFormat.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

  1. Initial Render: Cell displays the raw number formatted by Numbro (e.g., 350000 becomes $350,000.00)
  2. User clicks cell: The built-in numeric editor opens for editing
  3. User enters number: Input is validated against the numeric validator
  4. Validation: Non-numeric values are rejected; valid numbers are accepted
  5. 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.