Skip to content

devslab-kr/numkey

Repository files navigation

numkey

English | 한국어 · Live demo

npm CI license

"It's a string, but it's a number." Every business app has these fields — amounts, quantities, prices — and every team rebuilds the same input by hand: live thousands grouping (10,000), no leading zeros, right-aligned, numeric keypad on mobile, and a caret that doesn't jump while separators appear and disappear around it.

numkey is that input, done once:

  • Caret-safe live formatting — type into the middle of 1,234,567 and the cursor stays where you expect, even as groups reflow
  • String-first value model — the canonical value is a plain numeric string ("1234567.89"), never an IEEE 754 float, so money is safe
  • Leading-zero cleanup (0077), full-width digit normalization (123123 — Korean/Japanese IME), paste sanitizing (₩ 1,234원1234)
  • Right alignment + inputmode set automatically (opt-out available)
  • IME-safe — nothing runs mid-composition
  • Zero dependencies, TypeScript-first, ESM/CJS dual + a CDN global build
  • Works with plain <script> (JSP/PHP/anything server-rendered), Vue 3, and React

Wrong-keyboard-layout text in the same form? That's numkey's sibling, kokey.

No build step (JSP, PHP, static pages)

One script tag; everything else is markup. Inputs are auto-bound, including ones added to the DOM later.

<script src="https://cdn.jsdelivr.net/npm/@devslab/numkey"></script>

<input data-numkey>                            <!-- integers: 1,234,567 -->
<input data-numkey="2">                        <!-- 2 decimals: 1,234.56 -->
<input data-numkey data-numkey-negative>       <!-- minus allowed -->
<input data-numkey data-numkey-align="left">   <!-- keep left alignment -->

Server-rendered values (<input data-numkey value="1234567">) are formatted on load. To read the raw value back before submitting:

<script>
  const raw = numkey.getValue(document.querySelector('#amount')) // "1234567"
</script>

(Or simply strip separators server-side — the posted value is the display value.)

How the attributes work

data-numkey is the on-switch. It is what gets an input bound (auto-init watches input[data-numkey]), and its value doubles as the max decimal places — empty means integers only. Every other data-numkey-* attribute is an option that is only read from inputs that have data-numkey; on its own it does nothing:

<input data-numkey>                        <!-- ON, integers: 1,234,567 -->
<input data-numkey="2">                    <!-- ON, 2 decimals: 1,234.56 -->
<input data-numkey="2" data-numkey-negative>  <!-- options stack -->
<input data-numkey-locale="auto">          <!-- ✗ does NOTHING — no data-numkey -->
<input>                                    <!-- plain input, untouched -->
Attribute Meaning
data-numkey the switch — binds the input; the value is the max decimal places (empty = integer)
data-numkey-negative allow a leading minus
data-numkey-align="left" opt out of automatic right alignment
data-numkey-group="4" group size (default 3)
data-numkey-separator=" " group separator (default ,)
data-numkey-point="," decimal mark shown in the field (default .)
data-numkey-locale derive separators from a locale — see below

Locale-aware display (opt-in)

By default the display is deterministic: every visitor sees 1,234,567.89, whatever their browser is set to — which is what business forms usually need. data-numkey-locale opts a field into locale separators:

<!-- everyone sees 1,234,567.89 — the default, no locale involved -->
<input data-numkey="2">

<!-- follows the visitor's browser language:
     a German browser shows  1.234.567,89
     a Korean browser shows  1,234,567.89 -->
<input data-numkey="2" data-numkey-locale="auto">

<!-- pinned to German formatting for every visitor -->
<input data-numkey="2" data-numkey-locale="de-DE">

The locale changes only how the value is drawn. The canonical value is always "1234567.89"numkey.getValue(el) returns the same string in all three cases. Since a plain form POST submits the display value, a form using locales should read getValue into a hidden field (or normalize server-side) before submitting.

Use type="text" inputs. numkey sets inputmode so mobile keyboards show the numeric keypad; type="number" has no caret API and fights formatting.

npm

npm install @devslab/numkey
import { format, parse, bind, observe } from '@devslab/numkey'

format('1234567.5', { decimals: 2 })   // "1,234,567.5"
parse('₩ 1,234,567원')                  // "1234567"

bind(document.querySelector('#amount'), { decimals: 2 }) // one element
observe()                                                // all [data-numkey]

Vue 3

<script setup>
import { ref } from 'vue'
import { NumkeyInput, vNumkey } from '@devslab/numkey/vue'

const amount = ref('') // always canonical: "1234567"
</script>

<template>
  <!-- v-model gets the canonical value; the field shows 1,234,567 -->
  <NumkeyInput v-model="amount" :decimals="2" negative />

  <!-- or the directive for plain inputs -->
  <input v-numkey="2">
</template>

React

import { NumkeyInput, useNumkey } from '@devslab/numkey/react'

// Controlled: value/onValueChange speak canonical strings
const [amount, setAmount] = useState('')
<NumkeyInput value={amount} onValueChange={setAmount} decimals={2} negative />

// Uncontrolled: ref-callback hook
<input ref={useNumkey({ decimals: 2 })} defaultValue="1234567" />

API

Options

Option Default
decimals 0 max fraction digits (0 = integers only)
negative false allow a leading minus
group 3 digits per group (4 for 만-style grouping)
separator "," group separator in the display
decimalPoint "." decimal mark in the display (canonical always uses .)
locale opt-in: derive separator/decimalPoint via Intl"auto" (browser language) or a BCP 47 tag. Without it the display is deterministic no matter the visitor's browser, which is what business forms usually need. Explicit separator/decimalPoint win.

Core (pure functions)

parse(display, opts?) display/paste mess → canonical "1234567.89"
format(canonical, opts?) canonical → display "1,234,567.89"
finalize(canonical) settle transient typing states ("1234.""1234")

DOM

bind(el, opts?) attach live formatting; returns an unbind function
observe(root?) bind all [data-numkey] now and as they appear
getValue(el, opts?) canonical value of a bound input
setValue(el, canonical, opts?) write a canonical value as the formatted display
applyToInput(el, opts?) one caret-preserving reformat (building block)
createRefBinder(opts?) ref-callback factory for any framework

Notes

  • European formats work via options: { separator: '.', decimalPoint: ',' } displays 1.234.567,89 while the canonical value stays "1234567.89".
  • Backspacing directly over a separator moves the caret past it (the digit is deleted on the next backspace) — the same behavior as the major masking libraries. Smart separator-skipping deletion is on the roadmap.
  • Roadmap: Korean unit reading (1500000 → “150만”), 만/억 shorthand parsing (3만5천35000), hidden-field canonical sync for classic form posts.

License

MIT © devslab

About

Numeric input formatting — live thousands grouping, caret-safe editing, right alignment (숫자 인풋: 천단위 콤마·커서·정렬) — TypeScript, zero-dep, vanilla/Vue/React

Topics

Resources

License

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors