JavaScript Format Phone Number While Typing

JavaScript Format Phone Number While Typing
Project: Phone number input field
Author: Niels Voogt
Edit Online: View on CodePen
License: MIT

This code snippet helps you to create a format phone number while typing using javascript.  The function initializes a country dropdown menu and sets event listeners to open and close the menu. The function generates a list of countries and their prefixes and appends it to the dropdown menu. When a user selects a country, the function sets the selected country’s prefix, flag, and name in the appropriate fields. If the user clicks outside the dropdown menu or hovers out of the menu for more than 2 seconds, the function closes the menu.

Finally, the function also adds and removes a selected modifier class to the selected country in the dropdown menu. Overall, the code creates a country dropdown menu with event listeners to open, close, and select a country.

How to Create JavaScript Format Phone Number While Typing

First of all, load the following assets into the head tag of your HTML document.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css">

Create the HTML structure for the format phone as follows:

<!--
Recreation of dribble shot by Diana Palavandishvili
https://dribbble.com/shots/15474151-Phone-Number-Input-Field-Exploration
-->

<div class="pn-select" id="js_pn-select" style="--prefix-length: 2">
  <!-- Selected prefix -->
  <button class="pn-selected-prefix" aria-label="Select phonenumber prefix" id="js_trigger-dropdown" tabindex="1">
    <img class="pn-selected-prefix__flag" id="js_selected-flag" src="https://flagpedia.net/data/flags/icon/36x27/nl.png" />
    <!-- prettier-ignore -->
    <svg class="pn-selected-prefix__icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#081626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <polyline points="6 9 12 15 18 9" />
    </svg>
  </button>
  <!-- Phone number input -->
  <div class="pn-input">
    <label class="pn-input__label">Phonenumber*</label>
    <div class="pn-input__container">
      <input class="pn-input__prefix" value="+31" type="text" name="phonenumber-prefix" id="js_number-prefix" tabindex="-1" />
      <input class="pn-input__phonenumber" id="js_input-phonenumber" type="tel" name="phonenumber" pattern="\d*" value="" placeholder=" " autocomplete="nope" required max="10" tabindex="0" />
      <small class="pn-input__error">
        This is not a valid phone number
      </small>
    </div>
  </div>
  <!-- Dropdown -->
  <div class="pn-dropdown" id="js_dropdown">
    <div class="pn-search">
      <!-- prettier-ignore -->
      <svg class="pn-search__icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#103155" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="11" cy="11" r="8"></circle>
        <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
      </svg>
      <input placeholder="Search for countries" class="pn-search__input search" type="search" id="js_search-input" autocomplete="nope" />
    </div>
    <!-- Country list -->
    <ul class="pn-list list" id="js_list"></ul>
    <div class="pn-list-item pn-list-item--no-results" style="display: none" id="js_no-results-found">
      No results found
    </div>
  </div>
</div>

<div class="dribble-creds">
  Recreation of
  <a href="https://dribbble.com/shots/15474151-Phone-Number-Input-Field-Exploration" target="_blank">this</a>
  Dribbble shot
</div>

Now, style for the format phone using the following CSS styles:

@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&display=swap");
html {
  box-sizing: border-box;
  height: 100%;
  font-size: 16px;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

body {
  background: #efefef;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: "Inter", sans-serif;
  font-weight: 400;
  line-height: 1.5;
  padding: 0 1em;
  color: #081627;
}

input[type=search],
input[type=tel],
input[type=text] {
  font-size: 1rem;
  border: 0;
  font-family: inherit;
  outline: none;
  color: inherit;
  margin: 0;
  padding: 0;
  width: auto;
  max-width: 100%;
}
input[type=search]::-webkit-input-placeholder,
input[type=tel]::-webkit-input-placeholder,
input[type=text]::-webkit-input-placeholder {
  font-weight: 300;
  color: #6b7280;
}
input[type=search]::-moz-placeholder,
input[type=tel]::-moz-placeholder,
input[type=text]::-moz-placeholder {
  font-weight: 300;
  color: #6b7280;
}
input[type=search]:-ms-input-placeholder,
input[type=tel]:-ms-input-placeholder,
input[type=text]:-ms-input-placeholder {
  font-weight: 300;
  color: #6b7280;
}
input[type=search]:-moz-placeholder,
input[type=tel]:-moz-placeholder,
input[type=text]:-moz-placeholder {
  font-weight: 300;
  color: #6b7280;
}

:root {
  --border-radius: 0.75em;
  --border-color: #c3c3c3;
  --border-color-active: #0047a5;
  --dropdown-border-color: #eaeaec;
  --dropdown-trigger-background-color: #f3f5f9;
  --dropdown-trigger-hover-background-color: #e6eaf1;
  --input-error-color: #ff0000;
  --input-label-color: #85898f;
  --input-prefix-color: #656b73;
  --input-phonenumber-color: #081627;
  --list-item-hover-background: #f3f5f9;
}

.pn-select {
  position: relative;
  border-width: 1px;
  border-style: solid;
  border-color: var(--border-color);
  display: grid;
  grid-template-columns: 4.5em 1fr;
  border-radius: var(--border-radius);
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
  transition: all 0.2s ease-out;
  max-width: 20em;
  width: 100%;
  z-index: 1;
}
.pn-select:focus, .pn-select:focus-within {
  border-color: var(--border-color-active);
  box-shadow: 0 0 2px 0 var(--border-color-active);
}

.pn-dropdown {
  background: #ffffff;
  border-radius: var(--border-radius);
  border-width: 1px;
  border-style: solid;
  border-color: var(--dropdown-border-color);
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.135);
  opacity: 0;
  padding: 0 0.5em 0.5em;
  pointer-events: none;
  position: absolute;
  top: 140%;
  transform-origin: left top;
  transition: all 0.15s ease-out;
  width: 100%;
  visibility: hidden;
}
.pn-select--open .pn-dropdown {
  pointer-events: all;
  transform: none;
  opacity: 1;
  top: 120%;
  visibility: visible;
}

.pn-search {
  position: relative;
  display: flex;
  border-bottom-width: 1px;
  border-style: solid;
  border-color: var(--dropdown-border-color);
  margin-bottom: 0.5em;
}
.pn-search svg {
  display: block;
  height: 1.25rem;
  left: 0.5em;
  pointer-events: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 1.25rem;
}
.pn-search input[type=search] {
  padding-left: 2.5rem;
  height: 3rem;
  width: 100%;
}
.pn-search input[type=search]::-webkit-search-decoration, .pn-search input[type=search]::-webkit-search-cancel-button, .pn-search input[type=search]::-webkit-search-results-button, .pn-search input[type=search]::-webkit-search-results-decoration {
  display: none;
}

.pn-list {
  margin-right: -0.5em;
  max-height: 10.5em;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #ffffff #ffffff;
  position: relative;
}
.pn-list::-webkit-scrollbar {
  width: 10px;
}
.pn-list:hover {
  --scrollbar-background: #ffffff;
  --thumb-background: #c0c4ca;
  scrollbar-color: var(--thumb-background) var(--scrollbar-background);
}
.pn-list:hover::-webkit-scrollbar-track {
  background: var(--scrollbar-background);
}
.pn-list:hover::-webkit-scrollbar-thumb {
  background-color: var(--thumb-background);
  border-radius: 6px;
  border: 3px solid var(--scrollbar-background);
}
.pn-list--no-scroll {
  margin-right: 0;
}

.pn-selected-prefix {
  align-items: center;
  -webkit-appearance: none;
     -moz-appearance: none;
          appearance: none;
  background: var(--dropdown-trigger-background-color);
  border-radius: var(--border-radius) 0 0 var(--border-radius);
  border: 0;
  cursor: pointer;
  display: flex;
  justify-content: center;
  margin: 0;
  outline: none;
  padding: 0;
  transition: background 0.2s ease-out;
}
.pn-selected-prefix:hover, .pn-selected-prefix:focus {
  background: var(--dropdown-trigger-hover-background-color);
}
.pn-selected-prefix__flag {
  height: auto;
  width: 1.25rem;
}
.pn-selected-prefix__icon {
  display: block;
  height: 1.25rem;
  margin-left: 0.5em;
  margin-right: -0.25em;
  transition: all 0.15s ease-out;
  width: 1.25rem;
}
.pn-select--open .pn-selected-prefix__icon {
  transform: rotate(180deg);
}

.pn-input {
  background: #ffffff;
  border-radius: 0 var(--border-radius) var(--border-radius) 0;
  line-height: 1;
  overflow: hidden;
  padding: 0.5em 1em;
}
.pn-input__container {
  display: flex;
  flex-direction: row;
}
.pn-input__label {
  color: var(--input-label-color);
  font-size: 0.7rem;
  position: relative;
  top: -0.25em;
}
.pn-input__error {
  bottom: 0;
  color: var(--input-error-color);
  font-size: 0.785rem;
  left: 0;
  opacity: 0;
  pointer-events: none;
  position: absolute;
  transition: all 0.2s ease-out;
  z-index: -1;
}
.pn-input input[type=text] {
  background: transparent;
  position: absolute;
  color: var(--input-prefix-color);
  max-width: 3rem;
  pointer-events: none;
}
.pn-input input[type=tel] {
  color: var(--input-phonenumber-color);
  padding-left: calc(calc(var(--prefix-length) * 1ch) + 1.5ch);
  font-weight: 500;
}
.pn-input input[type=tel]:not(:-moz-placeholder-shown):invalid + .pn-input__error {
  opacity: 1;
  transform: translateY(175%);
}
.pn-input input[type=tel]:not(:-ms-input-placeholder):invalid + .pn-input__error {
  opacity: 1;
  transform: translateY(175%);
}
.pn-input input[type=tel]:not(:placeholder-shown):invalid + .pn-input__error {
  opacity: 1;
  transform: translateY(175%);
}

.pn-list-item {
  align-items: center;
  border-radius: 0.5em;
  display: flex;
  font-weight: 400;
  padding: 0.6em 0.75em;
  transition: background-color 0.2s ease-out;
  cursor: pointer;
  outline: none;
}
.pn-list-item__flag {
  width: 1.25em;
  height: auto;
  margin-right: 1em;
  display: block;
}
.pn-list-item__country {
  margin-right: 0.25em;
}
.pn-list-item:hover, .pn-list-item:focus {
  background-color: var(--list-item-hover-background);
}
.pn-list-item--selected {
  pointer-events: none;
  font-weight: 500;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23103155' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-check'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
  background-position: right 0.75em top 50%;
  background-repeat: no-repeat;
  background-size: 1.25rem;
  background-position: right 0.75em top 50%;
}
.pn-list-item--no-results {
  pointer-events: none;
}

.dribble-creds {
  bottom: 2em;
  font-size: 0.875rem;
  left: 0;
  position: fixed;
  right: 0;
  text-align: center;
}
.dribble-creds a {
  color: #ea4c89;
  text-decoration: underline;
}

Load the following scripts before closing the body tag:

<script src='https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js'></script>

Finally, add the following JavaScript function for its functionality:

const selectContainer = document.getElementById("js_pn-select");
const countrySearchInput = document.getElementById("js_search-input");
const noResultListItem = document.getElementById("js_no-results-found");
const dropdownTrigger = document.getElementById("js_trigger-dropdown");
const phoneNumberInput = document.getElementById("js_input-phonenumber");
const dropdownContainer = document.getElementById("js_dropdown");
const selectedPrefix = document.getElementById("js_number-prefix");
const selectedFlag = document.getElementById("js_selected-flag");
const listContainer = document.getElementById("js_list");

let countryList;

const init = async countries => {
  const selectCountry = e => {
    const { flag, prefix } = e.target.closest("li").dataset;
    setNewSelected(prefix, flag);
    closeDropdown();
    addSelectedModifier(flag);
  };

  // -------------- Update the 'Selected country flag' to reflect changes

  const setNewSelected = (prefix, flag) => {
    selectedFlag.src = `https://flagpedia.net/data/flags/icon/36x27/${flag}.png`;
    selectedPrefix.value = `+${prefix}`;
    selectContainer.style.setProperty("--prefix-length", prefix.length);
  };

  // -------------- Removes and adds modifier to selected country

  const addSelectedModifier = flag => {
    const previousSelected = document.getElementsByClassName(
    "pn-list-item--selected")[
    0];
    const newSelected = document.querySelectorAll(
    `.pn-list-item[data-flag=${flag}]`)[
    0];
    previousSelected.classList.remove("pn-list-item--selected");
    newSelected.classList.add("pn-list-item--selected");
  };

  // -------------- Close dropdown

  const closeDropdown = () => {
    selectContainer.classList.remove("pn-select--open");
    listContainer.scrollTop = 0;
    countrySearchInput.value = "";
    countryList.search();
    phoneNumberInput.focus();
    removeDropdownEvents();
  };

  // -------------- Open dropdown

  const openDropdown = () => {
    selectContainer.classList.add("pn-select--open");
    attatchDropdownEvents();
  };

  // -------------- Dropdown event listeners

  let countdown;

  const closeOnMouseLeave = () => {
    // console.log("countdown activated");
    countdown = setTimeout(() => closeDropdown(), 2000);
  };

  const clearTimeOut = () => clearTimeout(countdown);

  const attatchDropdownEvents = () => {
    // console.log("Adding event listeners");
    dropdownContainer.addEventListener("mouseleave", closeOnMouseLeave);
    dropdownContainer.addEventListener("mouseenter", clearTimeOut);
  };

  const removeDropdownEvents = () => {
    // console.log("Removing event listeners and countdown");
    clearTimeout(countdown);
    dropdownContainer.removeEventListener("mouseleave", closeOnMouseLeave);
    dropdownContainer.removeEventListener("mouseenter", clearTimeOut);
  };

  // -------------- Close when clicked outside the dropdown

  document.addEventListener("click", e => {
    if (
    e.target !== selectContainer &&
    !selectContainer.contains(e.target) &&
    selectContainer.classList.contains("pn-select--open"))
    {
      closeDropdown();
    }
  });

  // -------------- Append generated listItems to list element

  const createList = () =>
  new Promise((resolve, _) => {
    countries.forEach((country, index, countries) => {
      const { name, prefix, flag } = country;

      const element = `<li class="pn-list-item ${
      flag === "nl" ? "pn-list-item--selected" : ""
      } js_pn-list-item" data-flag="${flag}" data-prefix="${prefix}" tabindex="0" role="button" aria-pressed="false">
          <img class="pn-list-item__flag" src="https://flagpedia.net/data/flags/icon/36x27/${flag}.png" />
          <span class="pn-list-item__country js_country-name">${name}</span>
          <span class="pn-list-item__prefix js_country-prefix">(+${prefix})</span>
        </li>`;

      listContainer.innerHTML += element;

      if (index === countries.length - 1) {
        resolve();
      }
    });
  });

  // -------------- After all the listItems are created we loop over the items to attach the eventListeners

  const attatchListItemEventListeners = () =>
  new Promise((resolve, _) => {
    const listItems = [...document.getElementsByClassName("js_pn-list-item")];

    listItems.forEach((item, index, listItems) => {
      item.addEventListener("click", event => {
        selectCountry(event);
      });
      // Keydown event listener - https://dev.to/tylerjdev/when-role-button-is-not-enough-dac
      item.addEventListener("keydown", function (e) {
        const keyD = e.key !== undefined ? e.key : e.keyCode;
        if (
        keyD === "Enter" ||
        keyD === 13 ||
        ["Spacebar", " "].indexOf(keyD) >= 0 ||
        keyD === 32)
        {
          e.preventDefault();
          this.click();
        }
      });

      if (index === listItems.length - 1) {
        resolve();
      }
    });
  });

  // -------------- After all the listItems are created we initate list and it's listeners

  const initiateList = () => {
    countryList = new List("js_pn-select", {
      valueNames: ["js_country-name", "js_country-prefix"] });


    // Add 'updated' listener for search results
    countryList.on("updated", list => {
      if (list.matchingItems.length < 5)
      listContainer.classList.toggle("pn-list--no-scroll");

      noResultListItem.style.display =
      list.matchingItems.length > 0 ? "none" : "block";
    });
  };

  await createList();
  await attatchListItemEventListeners();
  initiateList();

  dropdownTrigger.addEventListener("click", () => {
    const isOpen = selectContainer.classList.contains("pn-select--open");
    isOpen ? closeDropdown() : openDropdown();
  });
};

const countries = [
{
  name: "Austria",
  prefix: 43,
  flag: "at" },

{
  name: "Belgium",
  prefix: 32,
  flag: "be" },

{
  name: "Bulgaria",
  prefix: 359,
  flag: "bg" },

{
  name: "Croatia",
  prefix: 385,
  flag: "hr" },

{
  name: "Cyprus",
  prefix: 357,
  flag: "cy" },

{
  name: "Czech Republic",
  prefix: 420,
  flag: "cz" },

{
  name: "Denmark",
  prefix: 45,
  flag: "dk" },

{
  name: "Estonia",
  prefix: 372,
  flag: "ee" },

{
  name: "Finland",
  prefix: 358,
  flag: "fi" },

{
  name: "France",
  prefix: 33,
  flag: "fr" },

{
  name: "Germany",
  prefix: 49,
  flag: "de" },

{
  name: "Greece",
  prefix: 30,
  flag: "gr" },

{
  name: "Hungary",
  prefix: 36,
  flag: "hu" },

{
  name: "Iceland",
  prefix: 354,
  flag: "is" },

{
  name: "Republic of Ireland",
  prefix: 353,
  flag: "ie" },

{
  name: "Italy",
  prefix: 39,
  flag: "it" },

{
  name: "Latvia",
  prefix: 371,
  flag: "lv" },

{
  name: "Liechtenstein",
  prefix: 423,
  flag: "li" },

{
  name: "Lithuania",
  prefix: 370,
  flag: "lt" },

{
  name: "Luxembourg",
  prefix: 352,
  flag: "lu" },

{
  name: "Malta",
  prefix: 356,
  flag: "mt" },

{
  name: "Netherlands",
  prefix: 31,
  flag: "nl" },

{
  name: "Norway",
  prefix: 47,
  flag: "no" },

{
  name: "Poland",
  prefix: 48,
  flag: "pl" },

{
  name: "Portugal",
  prefix: 351,
  flag: "pt" },

{
  name: "Romania",
  prefix: 40,
  flag: "ro" },

{
  name: "Slovakia",
  prefix: 421,
  flag: "sk" },

{
  name: "Slovenia",
  prefix: 386,
  flag: "si" },

{
  name: "Spain",
  prefix: 34,
  flag: "es" },

{
  name: "Sweden",
  prefix: 46,
  flag: "se" }];



init(countries);

That’s all! hopefully, you have successfully created our project. If you have any questions or suggestions, feel free to comment below.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *