Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

JoaoHamerski's avatar

Cleave.js deletes more characters than intended when using Vue and delimiters

First of all, here is the fiddle: https://jsfiddle.net/87e4hfc0/1/ Just type on input until a delimiter appears, so try to delete right when the cursor are on the delimiter and Cleave just deletes everthing that is before it until the previous delimiter.

I don't know why this is happening, my input is wrapped with a div and i'm using v-model in a component and followed the Vue documentation to that: https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components, this is why i didn't used v-model directly on my input.

0 likes
3 replies
rodrigo.pedra's avatar
Level 56

First of all, the Vue directive example on cleave.js repository is meant to be used directly on an <input> element, not on an wrapping component:

import Vue from 'vue'
import Cleave from 'cleave.js';

Vue.directive('cleave', {
    inserted: (el, binding) => {
        el.cleave = new Cleave(el, binding.value || {})
    },
    update: (el) => {
        const event = new Event('input', {bubbles: true});
        setTimeout(function () {
            el.value = el.cleave.properties.result
            el.dispatchEvent(event)
        }, 100);
    }
})

Reference: https://github.com/nosir/cleave.js/blob/master/doc/vue.md

Let's see your implementation:

// Vue directive as showed in official documentation:
// https://github.com/nosir/cleave.js/blob/master/doc/vue.md
Vue.directive('cleave', {
 inserted: (el, binding) => {
  let input = el.querySelector('input');
  input.cleave = new Cleave(input, binding.value || {})
 },
 update: (el) => {
  const input = el.querySelector('input');
  const event = new Event('input', {bubbles: true});

  setTimeout(function () {
   input.value = input.cleave.properties.result;
   input.dispatchEvent(event);
  }, 100);
 }
});

// Simple vue component
Vue.component('custom-input', {
	props: {value:{}},
  template: '<div> <input :value="value" @input="$emit(\'input\', $event.target.value)"> {{ value }}</div>'
});

// Root 
new Vue({
	props: ['foo'],
  el: '#app'
});
<div id="app">
  <custom-input v-cleave="{
        delimiters: ['.', '.', '-'],
        blocks: [3, 3, 3, 2],
        numericOnly: true
    }" v-model="foo"></custom-input>
</div>

Reference: https://jsfiddle.net/87e4hfc0/1/

Your first comment:

// Vue directive as showed in official documentation...

Is not accurate.

You changed the code sample from cleave.js' docs to query for a inner <input> element inside a component, whereas, as already said, the sample in the official docs is meant to be applied directly to an <input> element, not to an wrapping element.

What happens with your variant is as you expect the v-model to be applied in the component's outer <div>, and v-model listens for input events either from a form component, or a bubbling event from within a DOM element.

So when a user presses a key, the v-model catches the native input event. Upon updating the bound value (foo) it then calls the updated hook in your v-cleave directive variant. You directive implementation then dispatches a new input event from the component's inner <input> DOM element, which is captured again by the outer element v-model method again, leading to circular call-chain until the value stabilizes.

As you want to bind to an outer component you can try this:

Vue.directive('cleave', {
 inserted: (el, binding) => {
  let input = el.querySelector('input');
  input.cleave = new Cleave(input, binding.value || {})
 },
 update: (el, bindings, vnode) => {
  const input = el.querySelector('input');
  
  setTimeout(function () {
   input.value = input.cleave.properties.result;
   
   if (vnode.data.model) vnode.data.model.callback(input.value);
   if (vnode.data.on && vnode.data.on.input) vnode.data.on.input(input.value);
  }, 100);
 }
});

Instead of dispatching a new input event from the DOM, which would lead to the circular call-chain we already outlined, it checks if the vnode (the outer component) has any model handlers (v-model) or any input listeners bound to it. If there are any handlers it calls them directly.

Note this new modified v-cleave directive variant won't work if applied directly to an <input> element. As it searches for an <input> from the directive's root element on both inserted and updated hooks (el.querySelector('input')).

Hope it is clearer.

1 like
JoaoHamerski's avatar

It worked perfectly my brazilian fellow.

Sorry i didn't say that i changed the sample from cleave.js' docs, i though it was evident, i was not using v-cleave directly on a input but the logic was been applied to the input element, i though it would work as intended.

I'm just learning Vue now, didn't know about vnodes, i'll give a look on it.

Thank you so much for your well detailed explanation, helped me a lot.

1 like
rodrigo.pedra's avatar

Não há de que =)

Glad it worked out and that the response helped you better understand it.

Dealing with DOM events is a bit tricky, I guess this one of the reasons JavaScript frameworks got so popular, to abstract away this details.

As you said you are learning, this article is great to understand how to better integrate third-party libraries with Vue:

https://www.smashingmagazine.com/2019/02/vue-framework-third-party-javascript/

And the Cookbook and Example section on the Vue site have an un-measurable value on deep understanding Vue:

https://vuejs.org/v2/cookbook/index.html

https://vuejs.org/v2/examples/

Hope it helps.

Have a nice day =)

1 like

Please or to participate in this conversation.