Библиотека Cleave.js предназначена для форматирования пользовательского ввода в реальном времени: номера телефонов, банковских карт, дат, числовых значений и пользовательских шаблонов. В контексте Vue.js её интеграция требует учета реактивности, жизненного цикла компонентов и управления DOM через виртуальное дерево.
Cleave.js работает напрямую с DOM-элементом, что создаёт фундаментальное напряжение с подходом Vue, где DOM абстрагирован. Поэтому ключевой задачей становится синхронизация:
Наиболее прямолинейный способ подключения заключается в инициализации Cleave.js после монтирования компонента.
import Cleave from 'cleave.js';
export default {
name: 'PhoneInput',
data() {
return {
phone: ''
};
},
mounted() {
this.cleave = new Cleave(this.$refs.input, {
phone: true,
phoneRegionCode: 'US',
onValueChanged: (e) => {
this.phone = e.target.value;
}
});
}
};
$refs используется для прямого доступа к DOMonValueChanged служит мостом между Cleave.js и Vue
dataТакой подход плохо масштабируется, так как логика форматирования оказывается привязанной к компоненту.
Vue использует v-model как двустороннюю привязку.
Cleave.js также изменяет значение input, что приводит к потенциальным
конфликтам обновления.
Проблема проявляется в двух направлениях:
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
mounted() {
this.cleave = new Cleave(this.$refs.input, {
numeral: true,
onValueChanged: (e) => {
this.$emit('update:modelValue', e.target.rawValue);
}
});
},
watch: {
modelValue(newVal) {
if (this.cleave && newVal !== this.cleave.getRawValue()) {
this.cleave.setRawValue(newVal);
}
}
}
};
rawValue вместо formatted valueCleave.js различает два типа значений:
Пример использования:
onValueChanged: (e) => {
const formatted = e.target.value;
const raw = e.target.rawValue;
this.$emit('update:modelValue', raw);
this.formatted = formatted;
}
Такой подход позволяет:
Компонент можно обобщить для разных типов масок.
export default {
props: {
modelValue: String,
options: Object
},
emits: ['update:modelValue'],
mounted() {
this.initCleave();
},
methods: {
initCleave() {
this.cleave = new Cleave(this.$refs.input, {
...this.options,
onValueChanged: (e) => {
this.$emit('update:modelValue', e.target.rawValue);
}
});
}
},
watch: {
modelValue(val) {
if (this.cleave) {
this.cleave.setRawValue(val || '');
}
}
}
};
Cleave.js поддерживает несколько встроенных режимов:
{
phone: true,
phoneRegionCode: 'RU'
}
{
creditCard: true
}
{
numeral: true,
numeralThousandsGroupStyle: 'thousand'
}
{
delimiters: ['.', '.', '-'],
blocks: [3, 3, 4, 2]
}
Более чистый архитектурный подход — использование директив.
import Cleave from 'cleave.js';
export default {
mounted(el, binding) {
el.cleave = new Cleave(el, binding.value);
},
updated(el) {
el.cleave.setRawValue(el.value);
},
unmounted(el) {
el.cleave.destroy();
}
};
<input v-model="phone" v-cleave="options" />
Критически важным аспектом является корректное уничтожение экземпляра Cleave.js.
beforeUnmount() {
if (this.cleave) {
this.cleave.destroy();
}
}
Без этого возможны:
В Composition API интеграция становится более явной.
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import Cleave from 'cleave.js';
export default {
props: {
modelValue: String
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const input = ref(null);
let cleaveInstance = null;
onMounted(() => {
cleaveInstance = new Cleave(input.value, {
numeral: true,
onValueChanged: (e) => {
emit('update:modelValue', e.target.rawValue);
}
});
});
watch(() => props.modelValue, (val) => {
if (cleaveInstance) {
cleaveInstance.setRawValue(val || '');
}
});
onBeforeUnmount(() => {
cleaveInstance?.destroy();
});
return { input };
}
};
Vue обновляет DOM асинхронно, поэтому при сложных сценариях может возникать рассинхронизация.
Решение — использование nextTick:
import { nextTick } from 'vue';
watch(() => props.modelValue, async (val) => {
await nextTick();
cleaveInstance.setRawValue(val);
});
При изменении значения вне input важно обновлять Cleave напрямую:
setValue(val) {
this.cleave.setRawValue(val);
}
Cleave.js не всегда корректно обновляет конфигурацию «на лету», поэтому требуется пересоздание:
watch: {
options: {
deep: true,
handler() {
this.cleave.destroy();
this.initCleave();
}
}
}
Одновременное использование:
приводит к конфликту управления.
Компоненты, создающие Cleave.js без уничтожения, постепенно увеличивают нагрузку на DOM.
Это приводит к потере бизнес-логики и усложнению валидации.
Правильная архитектура разделяет:
Такое разделение снижает связанность и упрощает масштабирование форм.
В зрелой Vue-архитектуре Cleave.js выступает исключительно как форматирующий адаптер между DOM и бизнес-данными. Управление должно оставаться в Vue, тогда как Cleave.js отвечает только за визуальное преобразование ввода без вмешательства в состояние приложения.