Building a formatted text input that is less bad.


Problem Definition

Formatted text input fields are common practices in websites nowadays. However, they tradeoff editability for clarity most of the time. It does not have to be that way!. We are going to explore how we can improve editing experiences in this post.

For a better understanding of the problem, watching the video below is recommended.

The basic implementation

To format the text input values, we have to define at least two functions.

  1. To format the text input values
  2. To unformat the text input values
function formatPhoneNumber(unformattedText) {
  return [...unformattedText]
    .map((character, index) =>
      index !== unformattedText.length - 1 && (index === 1 || index === 4)
        ? character + " "
        : character
    )
    .join("");
}

function unformatPhoneNumber(formattedText) {
  return [...formattedText]
    .map((character) => (character === " " ? "" : character))
    .join("");
}

Then, we bind the function to an input event listener like so.

$input.addEventListener("input", (e) => {
  e.target.value = formatPhoneNumber(unformatPhoneNumber(e.target.value));
});

This will land us at the same experiences as in the video above. We can do better.

Handling selection

The positions of the cursor are related to the concept of selection. Input elements have properties called selectionStart and selectionEnd.

We can use these two properties to retrieve where the cursor was when the “input” event fired. Then, we can use setSelectionRange() to place the cursor at correct positions which will override the positions of the cursor set by the browser.

The input event handler becomes the following.

$input.addEventListener("input", (e) => {
  const editSelectionStart = $input.selectionStart;
  const editSelectionEnd = $input.selectionEnd;

  const isCursorAtLastCharacter =
    editSelectionStart === editSelectionEnd &&
    e.target.value.length === editSelectionStart;

  e.target.value = formatPhoneNumber(unformatPhoneNumber(e.target.value));

  if (!isCursorAtLastCharacter) {
    $input.setSelectionRange(editSelectionStart, editSelectionEnd);
  }
});

First, we saved the selection start and selection end positions. In case the cursor is at the end, we will use default behavior of the browser which will place the cursor at the end. Otherwise, we will set the cursor position manually.

Result

Here is the result. 🥳 With just a few lines of code, our formatted text input is great again.