Работа с Vue.js

Основные принципы работы Cleave.js во Vue-экосистеме

Библиотека Cleave.js предназначена для форматирования пользовательского ввода в реальном времени: номера телефонов, банковских карт, дат, числовых значений и пользовательских шаблонов. В контексте Vue.js её интеграция требует учета реактивности, жизненного цикла компонентов и управления DOM через виртуальное дерево.

Cleave.js работает напрямую с DOM-элементом, что создаёт фундаментальное напряжение с подходом Vue, где DOM абстрагирован. Поэтому ключевой задачей становится синхронизация:

  • состояния Vue (v-model)
  • состояния input DOM
  • внутреннего состояния Cleave.js

Базовая интеграция через mounted-хук

Наиболее прямолинейный способ подключения заключается в инициализации 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 используется для прямого доступа к DOM
  • onValueChanged служит мостом между Cleave.js и Vue data
  • реактивность поддерживается вручную

Ограничение архитектуры

Такой подход плохо масштабируется, так как логика форматирования оказывается привязанной к компоненту.

Связка с v-model и проблема синхронизации

Vue использует v-model как двустороннюю привязку. Cleave.js также изменяет значение input, что приводит к потенциальным конфликтам обновления.

Проблема проявляется в двух направлениях:

  • Vue обновляет input → Cleave перезаписывает формат
  • Cleave изменяет input → Vue перезаписывает значение

Решение через контролируемый компонент

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 value
  • синхронизация происходит через watcher
  • предотвращается зацикливание обновлений

Разделение formatted и raw значений

Cleave.js различает два типа значений:

  • formatted value — отображаемое пользователю
  • raw value — «чистое» значение без маски

Пример использования:

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]
}

Интеграция через Vue Directive

Более чистый архитектурный подход — использование директив.

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" />

Преимущества директив

  • отсутствие логики внутри компонентов
  • переиспользуемость
  • чистая интеграция с DOM-уровнем

Управление жизненным циклом

Критически важным аспектом является корректное уничтожение экземпляра Cleave.js.

beforeUnmount() {
  if (this.cleave) {
    this.cleave.destroy();
  }
}

Без этого возможны:

  • утечки памяти
  • дублирование обработчиков событий
  • некорректное поведение input после повторного монтирования

Работа с Vue 3 Composition API

В 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();
    }
  }
}

Частые архитектурные ошибки

Двойная привязка через v-model и value DOM

Одновременное использование:

  • v-model
  • прямого изменения input.value

приводит к конфликту управления.

Отсутствие destroy

Компоненты, создающие Cleave.js без уничтожения, постепенно увеличивают нагрузку на DOM.

Хранение formatted значения в state

Это приводит к потере бизнес-логики и усложнению валидации.

Рекомендованная структура слоя ввода

Правильная архитектура разделяет:

  • Vue state → raw value
  • Cleave.js → formatting layer
  • input DOM → presentation layer

Такое разделение снижает связанность и упрощает масштабирование форм.

Итоговая модель интеграции

В зрелой Vue-архитектуре Cleave.js выступает исключительно как форматирующий адаптер между DOM и бизнес-данными. Управление должно оставаться в Vue, тогда как Cleave.js отвечает только за визуальное преобразование ввода без вмешательства в состояние приложения.