Asia/Dhaka
Case Studies

Engineering Resilient Input Components for Real-World Data

image
A native number input can't group thousands or switch separator convention by locale; InputPrice.vue runs a format-strip-emit cycle on every keystroke and keeps the cursor stable through it.
I built InputPrice.vue for PreBook, a price input that reformats the visible string on every keystroke to match one of four separator conventions, while keeping the text cursor exactly where the user left it. The hard part wasn't the formatting - it was doing that live, per keystroke, without the field fighting the person typing into it.
Four separator conventions supported, eighteen commits across six weeks including a full rewrite, one cursor-stability fix, and separator bugs that recurred twice even after an optimization pass.
ProblemA price field has to show locale-correct grouping/decimal separators live while typing, without corrupting the value or the cursor
What I didBuilt a Vue component that reformats the display string on every input event and repositions the cursor based on separator count
Why it matteredPreBook's payment settings support four separator conventions - a native <input type="number"> supports none of them correctly
TeamSolo
A native number input can't group thousands, rejects locale-formatted strings like 1.234,56, and can't reconcile a styled display value against the plain decimal the backend expects.
A price field looks simple until you try to make it usable:
  • 1234.56 is technically valid but hard to scan - a merchant needs 1,234.56
  • Adding thousands grouping means two strings for one value: what's shown on screen, and the plain number sent to the backend, kept in sync on every keystroke
  • A native number input enforces one separator convention, browser-wide, and rejects anything else
  • PreBook lets each store pick its own separator convention, so type="number" was never going to work - it had to be a custom component
The literal string 1.234 means one thousand two hundred thirty-four under a dot-comma convention, or one point two three four under a comma-dot convention - the same characters, two different numbers.
  • 1.234 isn't one number - it's two, depending on the convention reading it: one thousand two hundred thirty-four under dot-comma, or one-point-two-three-four under comma-dot
  • There's no way to resolve that by inspecting the characters - the component has to be told which convention is active and stick to it
  • InputPrice.vue keeps a separatorMap for all four conventions PreBook supports (Comma-Dot, Dot-Comma, Space-Dot, Space-Comma), and every format/parse call reads from the store's price_separator setting
  • The cycle: format for display, strip back to a plain decimal on every keystroke, emit upward. Format, strip, emit, repeat
Reformatting a string after inserting a digit can insert a new thousands separator ahead of the cursor; restoring the pre-keystroke cursor index then leaves the cursor one character short of where the user actually left it.
  • Reformatting on every keystroke can change the string's length ahead of the cursor - typing a digit that pushes the integer part from three digits to four also inserts a new thousands separator
  • If the cursor just gets restored to its old index, it now sits one character short of where the user left it, so it visibly jumps backward every few keystrokes
  • The fix: compare how many thousands separators exist before and after each reformat. If a new one appeared ahead of the cursor, nudge the cursor forward by one; otherwise leave it alone
  • A small correction, but it's the difference between a field that feels broken and one that doesn't - and it only shows up once you test with real multi-digit prices, not single test values
Four judgment calls compressed: why not a native number input, why reformat every keystroke instead of on blur, why separator bugs kept recurring after an optimize pass, and why one component covers four locale conventions.
Why not just <input type="number">? It enforces one separator convention browser-wide, can't be configured per store, and rejects grouped thousands or a locale-specific decimal mark. Wrong problem for what PreBook needs. Why reformat on every keystroke instead of only on blur? Formatting only on blur leaves the field unreadable the entire time someone's typing - exactly when a merchant most needs to catch a mistyped digit. Live reformatting costs cursor-stability work, but that trade was worth it. Why did the separator bug keep coming back after the "optimize" commit? I was testing one locale convention at a time instead of running all four through each change - a coverage gap, not a one-off mistake. Why keep all four separator conventions in one component instead of splitting them out? One separatorMap means the reformat-and-cursor logic is written and fixed once, for every locale, instead of that same bug class multiplied across four separate components.
Part of the A Shared Component Architecture for a WordPress Booking Plugin series →

Related case studies

A Shared Component Architecture for a WordPress Booking Plugin - 4 Case Studies

A Shared Component Architecture for a WordPress Booking Plugin - 4 Case Studies

A solo, year-long build of a 44-component Vue library shared between two independently built apps inside a WordPress booking plugin - four linked case studies covering API boundaries, state, resilient input handling, and codebase structure.
Structuring a Large Frontend Codebase for Maintainability at Scale

Structuring a Large Frontend Codebase for Maintainability at Scale

Two independently-built Vue apps and a webpack-built Gutenberg block shared one component library through a consistent alias system and a per-app Tailwind purge - except in the one place plain CSS won.