JAVASCRIPT

Vue 3 Custom Directive for Real-time Input Masking

Learn to create a custom Vue 3 directive to automatically format user input in real-time, such as phone numbers or credit card numbers, enhancing user experience and data consistency.

// src/directives/inputMask.js
export default {
  mounted(el, binding) {
    const mask = binding.value; // e.g., '###-###-####' for phone
    let previousValue = '';

    const applyMask = () => {
      let value = el.value.replace(/\D/g, ''); // Remove non-digits
      let maskedValue = '';
      let maskIndex = 0;
      let valueIndex = 0;

      while (maskIndex < mask.length && valueIndex < value.length) {
        if (mask[maskIndex] === '#') {
          maskedValue += value[valueIndex];
          valueIndex++;
        } else {
          maskedValue += mask[maskIndex];
        }
        maskIndex++;
      }
      el.value = maskedValue;
      // Dispatch input event to ensure v-model updates
      el.dispatchEvent(new Event('input', { bubbles: true }));
    };

    el.addEventListener('input', applyMask);

    // Initial application if element already has a value
    if (el.value) {
      applyMask();
    }

    el.__v_inputMask_handler = applyMask; // Store handler for unmounted
  },
  updated(el, binding) {
    // Re-apply if mask changes, though less common for mask directives
    if (binding.value !== binding.oldValue) {
      // Remove old listener if exists
      if (el.__v_inputMask_handler) {
        el.removeEventListener('input', el.__v_inputMask_handler);
      }
      // Re-mount logic
      const mask = binding.value;
      const applyMask = () => {
        let value = el.value.replace(/\D/g, '');
        let maskedValue = '';
        let maskIndex = 0;
        let valueIndex = 0;

        while (maskIndex < mask.length && valueIndex < value.length) {
          if (mask[maskIndex] === '#') {
            maskedValue += value[valueIndex];
            valueIndex++;
          } else {
            maskedValue += mask[maskIndex];
          }
          maskIndex++;
        }
        el.value = maskedValue;
        el.dispatchEvent(new Event('input', { bubbles: true }));
      };
      el.addEventListener('input', applyMask);
      if (el.value) {
        applyMask();
      }
      el.__v_inputMask_handler = applyMask;
    }
  },
  unmounted(el) {
    if (el.__v_inputMask_handler) {
      el.removeEventListener('input', el.__v_inputMask_handler);
      delete el.__v_inputMask_handler;
    }
  }
};

// main.js or plugin file
/*
import { createApp } from 'vue';
import App from './App.vue';
import inputMaskDirective from './directives/inputMask';

const app = createApp(App);
app.directive('mask', inputMaskDirective);
app.mount('#app');
*/

// MyComponent.vue template usage:
/*
<template>
  <input v-model="phoneNumber" v-mask="'###-###-####'" placeholder="Phone Number" type="tel" />
  <input v-model="creditCard" v-mask="'#### #### #### ####'" placeholder="Credit Card" />
</template>

<script setup>
import { ref } from 'vue';
const phoneNumber = ref('');
const creditCard = ref('');
</script>
*/
How it works: This snippet demonstrates creating a Vue 3 custom directive named `v-mask`. It attaches an `input` event listener to the element in the `mounted` hook. The `applyMask` function cleans the input, applies the provided mask pattern (where `#` represents a digit), and updates the input's value. An `input` event is manually dispatched to ensure compatibility with `v-model`. The `unmounted` hook cleans up the event listener to prevent memory leaks. The `updated` hook handles cases where the mask pattern itself might change.

Need help integrating this into your project?

Our team of expert developers can help you build your custom application from scratch.

Hire DigitalCodeLabs