Vue Custom Directives and Practical Use Cases

In Vue, directives are special attributes prefixed with v- that apply reactive behavior directly to the DOM. While Vue provides built-in directives like v-model and v-show, developers can also define custom directives to encapsulate reusable DOM manipulation logic.

Custom directives can be registered globally or locally:

// Global registration
Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

// Local registration inside a component
export default {
  directives: {
    focus: {
      inserted(el) {
        el.focus();
      }
    }
  }
};

These directives support several lifecycle hooks:

  • bind: Called once when the directive is first bound to the element.
  • inserted: Called when the bound element has been inserted into its parent node.
  • update: Called when the component’s VNode is updated (before children).
  • componentUpdated: Called after the component’s VNode and its children have updated.
  • unbind: Called once when the directive is unbound from the element.

Each hook receives the following arguments:

  • el: The element the directive is bound to.
  • binding: An object containing:
    • name: Directive name (without v-).
    • value: The value passed to the directive.
    • oldValue: Previous value (available in update and componentUpdated).
    • expression: String representation of the binding expression.
    • arg: Optional argument passed to the directive (e.g., v-my:arg).
    • modifiers: Object of modifier flags (e.g., { foo: true } for .foo).
  • vnode: The virtual node produced by Vue’s compiler.
  • oldVnode: Previous virtual node (in update and componentUpdated).

Avoid mutating any argument except el. For cross-hook state, use the element’s dataset.

Example usage:

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', (el, binding) => {
  console.log(binding.value.color); // 'white'
  console.log(binding.value.text);  // 'hello!'
});

Common Use Cases

Debounced Click Handler

Prevent rapid clicks using a throttle-like directive:

Vue.directive('throttle', {
  bind(el, binding) {
    const delay = binding.value || 2000;
    let timer = null;

    const handleClick = (e) => {
      if (timer) {
        e.stopImmediatePropagation();
        return;
      }
      timer = setTimeout(() => {
        timer = null;
      }, delay);
    };

    el.addEventListener('click', handleClick, true);
    el._throttleHandler = handleClick;
  },
  unbind(el) {
    el.removeEventListener('click', el._throttleHandler, true);
  }
});

Use in template:

<button v-throttle="1500" @click="submit">Submit</button>

Image Lazy Loading

Load images only when they enter the viewport:

const LazyLoad = {
  install(Vue, { default: placeholder }) {
    Vue.directive('lazy', {
      bind(el, { value }) {
        el.dataset.src = value;
        el.src = placeholder;
      },
      inserted(el) {
        if ('IntersectionObserver' in window) {
          const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
              if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.removeAttribute('data-src');
                observer.unobserve(img);
              }
            });
          });
          observer.observe(el);
        } else {
          // Fallback scroll-based loading
          const load = () => {
            const rect = el.getBoundingClientRect();
            const inView = rect.top < window.innerHeight && rect.bottom >= 0;
            if (inView && el.dataset.src) {
              el.src = el.dataset.src;
              el.removeAttribute('data-src');
            }
          };
          load();
          window.addEventListener('scroll', load);
          el._lazyScrollHandler = load;
        }
      },
      unbind(el) {
        if (el._lazyScrollHandler) {
          window.removeEventListener('scroll', el._lazyScrollHandler);
        }
      }
    });
  }
};

Vue.use(LazyLoad, { default: '/loading.gif' });

Template usage:

<img v-lazy="/real-image.jpg" />

One-Click Copy

Copy text to clipboard on click:

const copyDirective = {
  bind(el, { value }) {
    el._copyValue = value;
    el._copyHandler = () => {
      if (!el._copyValue) return;

      const textarea = document.createElement('textarea');
      textarea.value = el._copyValue;
      textarea.readOnly = true;
      textarea.style.cssText = 'position:absolute;left:-9999px;';
      document.body.appendChild(textarea);

      textarea.select();
      const success = document.execCommand('copy');
      document.body.removeChild(textarea);

      if (success) alert('Copied!');
    };
    el.addEventListener('click', el._copyHandler);
  },
  componentUpdated(el, { value }) {
    el._copyValue = value;
  },
  unbind(el) {
    el.removeEventListener('click', el._copyHandler);
  }
};

Vue.directive('copy', copyDirective);

Usage:

<span v-copy="'Hello world!'">Copy this</span>

Other practical applications include drag-and-drop handlers, dynamic watermarking, and fine-grained permission controls based on user roles.

Tags: vue Custom Directives DOM Manipulation Lazy Loading Debounce

Posted on Sat, 31 Oct 2026 16:16:03 +0000 by alex.saidani