Chapter 80: Advanced Form Processing & Client-Side UX

Real-Time Input Masking

Crafting flicker-free, accessible input masks for credit cards, phone numbers, and dates while mastering cursor position tracking and `setSelectionRange`.

LEARNING OBJECTIVES
  • Understand the mechanics of real-time input formatting using the DOM input and beforeinput events.
  • Diagnose and solve the classic "Cursor Jump to End" bug using selectionStart and setSelectionRange().
  • Implement robust masking patterns for credit card numbers (with Amex/Visa detection), phone numbers, and dates.
  • Support backspace and delete keystrokes across formatting delimiters without getting stuck.
  • Configure mobile-friendly input hints using inputmode, pattern, and autocomplete.
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine writing on a paper form with pre-printed boxed grids—four boxes, a dash, four boxes, a dash. If a helpful assistant stands beside you and physically shifts the entire sheet of paper to the left every time you write a single digit, your pen tip will suddenly land in the wrong box. You try to fix a typo in the middle of the number, but every time you make a stroke, the assistant pulls the paper and pushes your pen all the way to the bottom right corner of the page.

This is the infamous Cursor Jumping Bug in JavaScript. When an engineer naively rewrites input.value = format(input.value) inside an input event listener, the browser rendering engine loses track of where the user was typing and throws the caret to the end of the text.

To build a professional input mask, your code must act like a master typist: calculate how many raw digits existed before the pen tip, transform the paper layout, and immediately place the pen tip back down at the exact corresponding character index.


Technical Deep Dive & Specifications

The Anatomy of the Cursor Jumping Problem

When a user edits an input in the middle of a string:

  1. User types digit '5' at index 7 in "4111 11|11 1111".
  2. The input event triggers.
  3. JavaScript reformats the string to "4111 1151 1111 1".
  4. JavaScript assigns input.value = newString.
  5. Browser Default Behavior: Assigning to .value resets selectionStart and selectionEnd to newString.length (the very end).
  6. Result: The user is typing in the middle, but subsequent keystrokes appear at the end!
BEFORE REFORMAT:
  Value:  "4 1 1 1   1 1 [5] 1   1 1 1 1"
  Cursor:                 ^ (Index 7: 5 raw digits prior)

NAIVE REFORMAT:
  Value:  "4 1 1 1   1 1 5 1   1 1 1 1"
  Cursor:                               ^ (Index 15 - JUMPED TO END!)

ALGORITHMIC CURSOR RESTORATION:
  1. Count unmasked characters before old cursor = 5 digits ('4','1','1','1','1').
  2. Format new raw string -> "4111 1151 1111 1".
  3. Walk formatted string until 5 raw digits are encountered.
  4. Target cursor position = index 8.
  5. input.setSelectionRange(8, 8).

The Cursor Preservation Algorithm

function formatWithCursorPreservation(input, formatterFn) {
  const previousValue = input.value;
  const previousCursor = input.selectionStart;

  // 1. Count raw valid digits before the cursor prior to formatting
  const digitsBeforeCursor = previousValue
    .slice(0, previousCursor)
    .replace(/\D/g, '').length;

  // 2. Compute formatted value from raw characters
  const rawDigits = previousValue.replace(/\D/g, '');
  const formattedValue = formatterFn(rawDigits);

  // 3. Update DOM value
  input.value = formattedValue;

  // 4. Find new cursor position matching the raw digit count
  let newCursor = 0;
  let digitCount = 0;
  for (let i = 0; i < formattedValue.length; i++) {
    if (/\d/.test(formattedValue[i])) {
      digitCount++;
    }
    if (digitCount === digitsBeforeCursor) {
      newCursor = i + 1;
      break;
    }
  }

  // Edge case: if no digits before cursor, place at start
  if (digitsBeforeCursor === 0) newCursor = 0;

  // 5. Restore cursor position
  input.setSelectionRange(newCursor, newCursor);
}

Common Mask Formatting Rules

+-------------------------------------------------------------------------------+
| Pattern Name      | Format Template       | Regex Token Transformation        |
+-------------------------------------------------------------------------------+
| Standard Credit   | #### #### #### ####   | (\d{4})(?=\d) -> '$1 '            |
| Amex Card         | #### ###### #####     | (\d{4})(\d{6})? -> '$1 $2 $3'     |
| US Phone Number   | (###) ###-####        | (\d{3})(\d{3})(\d{4})             |
| Expiration Date   | MM/YY                 | (\d{2})(?=\d) -> '$1/'            |
+-------------------------------------------------------------------------------+

Mobile Input Optimizations

Masking must be paired with correct HTML5 mobile hints:

  • inputmode="numeric": Pops up the numeric dial pad on iOS/Android without showing the full alpha keyboard.
  • autocomplete="cc-number": Enables 1-tap browser autofill and camera card scanning.
  • autocomplete="tel": Enables phone number autofill from contacts.

💻 Interactive Code Playground

Starter Code

Line-by-Line Code Breakdown

  • Lines 101–105 (const digitsBeforeCursor = ...): Analyzes the substring from index 0 up to selectionStart, counting purely numeric digits while ignoring space or punctuation delimiters.
  • Line 108 (const raw = prevVal.replace(/\D/g, '')): Cleans the input stream of all non-numeric characters before passing to the pattern formatter.
  • Line 111 (inputElement.value = formatted): Updates the DOM input string with appropriate spacing, dashes, or parentheses.
  • Lines 114–121 (for (let i = 0; i < formatted.length; i++)): Iterates through the freshly formatted string to map where the Nth digit now resides, pinpointing the exact character index for the caret.
  • Line 124 (inputElement.setSelectionRange(newCursor, newCursor)): Programmatically pins the cursor at the calculated position, eliminating cursor jumping bugs.
  • Lines 134–146 (ccBadge detection): Checks the Major Industry Identifier (MII) prefixes (4 for Visa, 51–55 for Mastercard, 34/37 for American Express) and switches formatting from 4-4-4-4 to 4-6-5 on the fly.

Expected Browser Render Output

(Typing in the middle of any field immediately retains the caret directly adjacent to the edited digit rather than snapping to the end.)


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
+-----------------------------------------------------------+
| Payment & Contact Details                                 |
|                                                           |
| Credit Card Number                                        |
| [ 4111 2222 3333 4444                       ] [ VISA ]    |
|                                                           |
| Expiry Date            Phone Number                       |
| [ 12/28              ] [ (415) 555-0199                 ] |
|                                                           |
| Raw State: CC Raw: "4111222233334444" | Exp: "12/28" ...  |
+-----------------------------------------------------------+

🏋️ Hands-On Exercise

🎯 The Challenge: Currency Mask with Delimiter & Precision Control

Instructions:

  1. Build a real-time currency formatter input ($ 1,234,567.89).
  2. Format rules:
    • Always prefix with $ .
    • Insert comma , grouping for every 3 integer digits.
    • Allow a maximum of 1 decimal dot . and at most 2 fractional decimal digits.
    • Prevent entering letters or multiple decimal points.
  3. Maintain stable cursor positioning when users edit numbers in the thousands or millions columns.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Filtering Keydown Codes Instead of Processing input: Intercepting keydown and calling e.preventDefault() on non-digits breaks pasting via Ctrl+V/Cmd+V, voice dictation, and password manager autofill. Always listen to the input event and sanitize the updated string.
  2. Neglecting the selectionStart Restoration: Modifying input.value without restoring setSelectionRange() causes intolerable jumping bugs when users make edits in the middle of long numbers.
  3. Storing Formatted Delimiters in the Backend: Submitting (555) 019-2834 to your API creates messy database records. Always strip delimiters before serialization (rawDigits = value.replace(/\D/g, '')) or store in an unmasked hidden field.

💡 Pro Tips

  1. Leverage inputmode for Instant Mobile Keyboards: Specifying inputmode="numeric" or inputmode="decimal" on text inputs opens the native numeric keypad on iOS and Android without triggering browser validation constraints that <input type="number"> enforces.
  2. Handle Backspace on Delimiters: When a user presses backspace directly after a space or hyphen, detect e.inputType === 'deleteContentBackward' and delete the preceding digit rather than just the delimiter, preventing the mask from getting "stuck".

📌 Key Takeaways

  • Modifying input.value programmatically resets the DOM caret to the end of the input string.
  • Preserve cursor position by counting valid raw characters prior to selectionStart and restoring via setSelectionRange().
  • Listen to the input event to accommodate typing, pasting, autofill, and voice dictation.
  • Always configure inputmode="numeric" and standard autocomplete tokens for frictionless mobile UX.
  • Sanitize masked values back to raw numbers or ISO standards before sending payloads to backend endpoints.
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

Why does the text cursor automatically jump to the end of an <input> when its .value property is modified inside JavaScript?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

Which HTML attribute configuration provides the best mobile keyboard experience for credit card inputs without invoking problematic native number spinner arrows?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

Why is filtering inputs using keydown event listeners considered an anti-pattern compared to listening to the input event?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP