Vue Custom Directive for jQuery Datepicker

Integrating legacy jQuery UI plugins into modern Vue.js applications can be accomplished cleanly without polluting component logic by using custom directives. This guide demonstrates how to create a reusable custom Vue directive that initializes a jQuery datepicker, synchronizes date selections with Vue's reactivity system, and properly destroys the plugin instance to prevent memory leaks.

Prerequisites

Ensure jQuery and jQuery UI are installed and imported into your project:

npm install jquery jquery-ui-dist

1. Creating the Custom Directive

In Vue 3, custom directives provide lifecycle hooks such as mounted and beforeUnmount. Create a new file named vDatepicker.js:

import $ from 'jquery';
import 'jquery-ui-dist/jquery-ui';
import 'jquery-ui-dist/jquery-ui.css';

export default {
  mounted(el, binding) {
    // Initialize the jQuery datepicker on the element
    $(el).datepicker({
      dateFormat: 'yy-mm-dd',
      // Pass any options passed as the directive value
      ...binding.value,
      onSelect: function (dateText) {
        // Dispatch native input and change events so v-model updates
        el.value = dateText;
        el.dispatchEvent(new Event('input', { bubbles: true }));
        el.dispatchEvent(new Event('change', { bubbles: true }));
      }
    });
  },

  beforeUnmount(el) {
    // Clean up the jQuery UI instance to avoid memory leaks
    if ($(el).data('datepicker')) {
      $(el).datepicker('destroy');
    }
  }
};

2. Registering the Directive

You can register the directive globally in your main.js file:

import { createApp } from 'vue';
import App from './App.vue';
import vDatepicker from './directives/vDatepicker';

const app = createApp(App);
app.directive('datepicker', vDatepicker);
app.mount('#app');

Alternatively, register it locally within a single component:

import vDatepicker from '@/directives/vDatepicker';

export default {
  directives: {
    datepicker: vDatepicker
  }
};

3. Using the Directive in a Component

Apply the directive to a standard <input> element alongside v-model. You can also pass jQuery UI configuration options directly into the directive expression:

<template>
  <div>
    <label for="birthdate">Select Date:</label>
    <input 
      id="birthdate" 
      type="text" 
      v-datepicker="{ minDate: 0, maxDate: '+1M' }" 
      v-model="selectedDate" 
      placeholder="Select a date" 
    />
    <p>Selected Date: {{ selectedDate }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const selectedDate = ref('');
</script>

Key Considerations

  • Two-Way Binding: Because jQuery modifies the DOM directly, the standard Vue v-model listener does not detect the change automatically. Manually triggering the input event inside onSelect solves this problem.
  • Component Cleanup: Always destroy the jQuery plugin inside beforeUnmount (or unbind in Vue 2) to free references and avoid lingering event listeners when the element leaves the DOM.