Работа с Vue.js

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

Наиболее частые проблемы:

  • потеря курсора при вводе;
  • бесконечные циклы обновлений;
  • дублирование форматирования;
  • некорректная синхронизация v-model;
  • уничтожение экземпляров при перерисовке компонентов;
  • проблемы с SSR и Hydration.

Базовая интеграция AutoNumeric во Vue

Установка библиотек

Через npm

npm install autonumeric

Подключение

import AutoNumeric from 'autonumeric';

Использование в компоненте Options API

Простейший пример

<template>
  <input ref="priceInput" type="text">
</template>

<script>
import AutoNumeric from 'autonumeric';

export default {
  mounted() {
    this.anElement = new AutoNumeric(
      this.$refs.priceInput,
      {
        currencySymbol: '₸ ',
        decimalCharacter: ',',
        digitGroupSeparator: ' '
      }
    );
  },

  beforeUnmount() {
    if (this.anElement) {
      this.anElement.remove();
    }
  }
};
</script>

Инициализация в mounted

Создание экземпляра AutoNumeric должно происходить только после появления DOM-элемента.

Правильный хук:

mounted()

Неправильно:

created()
beforeMount()
setup()

До выполнения mounted() ссылка ref ещё отсутствует.


Удаление экземпляра

AutoNumeric регистрирует события:

  • input
  • keydown
  • focus
  • blur
  • wheel

Если не удалить экземпляр при уничтожении компонента, возникают:

  • утечки памяти;
  • дублирование обработчиков;
  • ошибки при повторном создании компонента.

Правильное удаление:

beforeUnmount() {
  this.anElement.remove();
}

Работа с v-model

Основная проблема

Vue ожидает обычное значение поля:

12345

AutoNumeric отображает форматированную строку:

12 345,00 ₸

Из-за этого прямое использование v-model приводит к конфликтам.


Неправильный вариант

<input v-model="price">
mounted() {
  new AutoNumeric(this.$refs.input);
}

Vue пытается обновлять значение поля одновременно с AutoNumeric.

Последствия:

  • прыгающий курсор;
  • двойное форматирование;
  • потеря данных;
  • неправильный ввод.

Правильная схема синхронизации

Отдельное хранение сырого значения

<template>
  <input ref="priceInput" type="text">
</template>

<script>
import AutoNumeric from 'autonumeric';

export default {
  data() {
    return {
      price: null
    };
  },

  mounted() {
    this.anElement = new AutoNumeric(this.$refs.priceInput);

    this.$refs.priceInput.addEventListener('autoNumeric:rawValueModified', () => {
      this.price = this.anElement.getNumber();
    });
  }
};
</script>

Использование событий AutoNumeric

Основные события

Событие Назначение
autoNumeric:initialized экземпляр создан
autoNumeric:formatted выполнено форматирование
autoNumeric:rawValueModified изменилось raw-значение
autoNumeric:minExceeded превышен минимум
autoNumeric:maxExceeded превышен максимум
autoNumeric:invalidValue введено некорректное значение

Отслеживание изменения значения

this.$refs.priceInput.addEventListener(
  'autoNumeric:rawValueModified',
  (event) => {
    console.log(event.detail.newRawValue);
  }
);

Работа с Composition API

Базовый пример

<template>
  <input ref="inputRef" type="text">
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import AutoNumeric from 'autonumeric';

const inputRef = ref(null);
let anElement = null;

onMounted(() => {
  anElement = new AutoNumeric(inputRef.value, {
    currencySymbol: '$ '
  });
});

onBeforeUnmount(() => {
  if (anElement) {
    anElement.remove();
  }
});
</script>

Реактивное обновление значения

Синхронизация с ref

<script setup>
import { ref, onMounted } from 'vue';
import AutoNumeric from 'autonumeric';

const amount = ref(0);

const inputRef = ref(null);

let an = null;

onMounted(() => {
  an = new AutoNumeric(inputRef.value);

  inputRef.value.addEventListener(
    'autoNumeric:rawValueModified',
    () => {
      amount.value = an.getNumber();
    }
  );
});
</script>

Двустороннее связывание через defineModel

Реализация полноценного компонента

<template>
  <input ref="inputRef" type="text">
</template>

<script setup>
import { ref, onMounted, watch } from 'vue';
import AutoNumeric from 'autonumeric';

const model = defineModel();

const inputRef = ref(null);

let an = null;

onMounted(() => {
  an = new AutoNumeric(inputRef.value);

  inputRef.value.addEventListener(
    'autoNumeric:rawValueModified',
    () => {
      model.value = an.getNumericString();
    }
  );
});

watch(model, (value) => {
  if (an && value !== an.getNumericString()) {
    an.set(value);
  }
});
</script>

Создание переиспользуемого компонента

Компонент CurrencyInput.vue

<template>
  <input ref="inputRef" type="text">
</template>

<script>
import AutoNumeric from 'autonumeric';

export default {
  props: {
    modelValue: {
      type: [String, Number],
      default: ''
    },

    options: {
      type: Object,
      default: () => ({})
    }
  },

  emits: ['update:modelValue'],

  mounted() {
    this.anElement = new AutoNumeric(
      this.$refs.inputRef,
      this.modelValue,
      this.options
    );

    this.$refs.inputRef.addEventListener(
      'autoNumeric:rawValueModified',
      () => {
        this.$emit(
          'update:modelValue',
          this.anElement.getNumericString()
        );
      }
    );
  },

  watch: {
    modelValue(value) {
      if (
        this.anElement &&
        value !== this.anElement.getNumericString()
      ) {
        this.anElement.set(value);
      }
    }
  },

  beforeUnmount() {
    this.anElement.remove();
  }
};
</script>

Использование компонента

<template>
  <CurrencyInput
    v-model="price"
    :options="currencyOptions"
  />
</template>

<script>
import CurrencyInput from './CurrencyInput.vue';

export default {
  components: {
    CurrencyInput
  },

  data() {
    return {
      price: 15000,

      currencyOptions: {
        currencySymbol: '₸ ',
        decimalCharacter: ',',
        digitGroupSeparator: ' '
      }
    };
  }
};
</script>

Работа с watch

Синхронизация внешних изменений

Vue может менять значение извне:

this.price = 100000;

AutoNumeric не узнает об изменении автоматически.

Необходим watch.


Пример корректного watch

watch: {
  price(newValue) {
    this.anElement.set(newValue);
  }
}

Предотвращение циклических обновлений

Проблема

watch: {
  price(newValue) {
    this.anElement.set(newValue);
  }
}

set() вызывает изменение значения.

Изменение значения вызывает событие.

Событие меняет price.

Получается цикл.


Правильная проверка

watch: {
  price(newValue) {
    const current = this.anElement.getNumericString();

    if (newValue !== current) {
      this.anElement.set(newValue);
    }
  }
}

Использование computed

Форматированное и сырое значение

computed: {
  formattedPrice() {
    return this.anElement
      ? this.anElement.getFormatted()
      : '';
  }
}

Работа с несколькими полями

Инициализация массива экземпляров

<template>
  <div v-for="item in items" :key="item.id">
    <input :ref="setInputRef">
  </div>
</template>

<script>
import AutoNumeric from 'autonumeric';

export default {
  data() {
    return {
      refs: [],
      autoNumerics: []
    };
  },

  methods: {
    setInputRef(el) {
      if (el) {
        this.refs.push(el);
      }
    }
  },

  mounted() {
    this.autoNumerics = this.refs.map((el) => {
      return new AutoNumeric(el);
    });
  }
};
</script>

Проблема v-for и ref

При перерисовке массива:

  • элементы удаляются;
  • refs пересоздаются;
  • экземпляры теряют связь с DOM.

Правильный способ

Использование дочернего компонента

<div v-for="item in items" :key="item.id">
  <CurrencyInput v-model="item.price" />
</div>

Каждый компонент управляет собственным экземпляром AutoNumeric.


Динамическое изменение настроек

Изменение options

watch: {
  options: {
    deep: true,

    handler(newOptions) {
      this.anElement.update(newOptions);
    }
  }
}

Пример смены валюты

this.currencyOptions = {
  currencySymbol: '$ '
};

Использование provide/inject

Глобальные настройки форматирования

provide() {
  return {
    autoNumericDefaults: {
      digitGroupSeparator: ' ',
      decimalCharacter: ','
    }
  };
}

Получение настроек

inject: ['autoNumericDefaults']

Создание composable

useAutoNumeric.js

import {
  ref,
  onMounted,
  onBeforeUnmount
} from 'vue';

import AutoNumeric from 'autonumeric';

export function useAutoNumeric(options = {}) {
  const inputRef = ref(null);

  let an = null;

  onMounted(() => {
    an = new AutoNumeric(
      inputRef.value,
      options
    );
  });

  onBeforeUnmount(() => {
    if (an) {
      an.remove();
    }
  });

  return {
    inputRef,
    getInstance: () => an
  };
}

Использование composable

<script setup>
import { useAutoNumeric } from './useAutoNumeric';

const {
  inputRef,
  getInstance
} = useAutoNumeric({
  currencySymbol: '€ '
});
</script>

<template>
  <input ref="inputRef">
</template>

SSR и Nuxt

Основная проблема

AutoNumeric использует:

window
document
HTMLElement

На сервере этих объектов нет.


Ошибка SSR

window is not defined

Решение для Nuxt

Клиентская инициализация

onMounted(async () => {
  const AutoNumeric = (await import('autonumeric')).default;

  new AutoNumeric(inputRef.value);
});

Использование ClientOnly

<ClientOnly>
  <CurrencyInput />
</ClientOnly>

Lazy loading

Динамический импорт

const AutoNumeric = await import('autonumeric');

Преимущества:

  • уменьшение initial bundle;
  • ускорение SSR;
  • загрузка по требованию.

Использование директив Vue

Создание директивы v-autonumeric

import AutoNumeric from 'autonumeric';

export default {
  mounted(el, binding) {
    el.anElement = new AutoNumeric(
      el,
      binding.value || {}
    );
  },

  upd ated(el, binding) {
    el.anElement.update(binding.value);
  },

  unmounted(el) {
    el.anElement.remove();
  }
};

Регистрация директивы

app.directive('autonumeric', autonumericDirective);

Использование директивы

<input
  v-autonumeric="{
    currencySymbol: '$ '
  }"
>

Ограничения директив

Директива подходит для:

  • простого форматирования;
  • статических полей;
  • базовой интеграции.

Недостатки:

  • сложнее реализовать v-model;
  • нет изоляции состояния;
  • сложнее тестировать;
  • ограниченная реактивность.

Работа с Pinia

Хранение raw-значения

export const usePaymentStore = defineStore('payment', {
  state: () => ({
    amount: 0
  })
});

Обновление store

input.addEventListener(
  'autoNumeric:rawValueModified',
  () => {
    store.amount = an.getNumber();
  }
);

Почему нельзя хранить форматированную строку

Неправильно:

"15 000,00 ₸"

Правильно:

15000

Форматирование должно быть задачей UI.


Валидация с VeeValidate

Интеграция с raw-значением

const value = an.getNumber();

Пример правила

value => value > 0

Ошибка при валидации форматированной строки

"10 000 ₽" > 0

Такое сравнение некорректно.


Работа с form submit

Получение сырого значения

const amount = an.getNumericString();

Получение форматированного значения

const formatted = an.getFormatted();

Разница между методами

Метод Результат
getNumericString() "15000.5"
getNumber() 15000.5
getFormatted() "15 000,50 ₽"

Использование nextTick

Проблема

Vue обновляет DOM асинхронно.

AutoNumeric может обратиться к устаревшему элементу.


Решение

await nextTick();

anElement.se t(value);

Работа с Teleport

При переносе элементов через:

<Teleport>

DOM-узел меняет положение.

Иногда требуется повторная инициализация.


Работа с KeepAlive

Повторная активация компонента

activated() {
  this.anElement.reformat();
}

Suspense и асинхронные компоненты

AutoNumeric должен инициализироваться только после полной загрузки компонента.

Правильный хук:

onMounted()

Тестирование компонентов

Vue Test Utils

import { mount } from '@vue/test-utils';

Проверка raw-значения

expect(wrapper.vm.price).toBe('15000');

Проверка форматирования

expect(
  input.element.value
).toContain('15 000');

Типизация TypeScript

Тип экземпляра

import AutoNumeric from 'autonumeric';

let an: AutoNumeric | null = null;

Типизация props

interface Props {
  modelValue: string | number;
}

Проблемы производительности

Частые причины

Массовая инициализация

1000+ полей

Глубокие watch

deep: true

Постоянные upd ate()

an.update(...)

Оптимизация

debounce обновлений

import debounce from 'lodash/debounce';

Ленивое создание

if (isVisible) {
  new AutoNumeric(...)
}

Переиспользование конфигураций

const currencyOptions = Object.freeze({
  currencySymbol: '$ '
});

Типичные ошибки интеграции

Использование v-model напрямую

<input v-model="price">

Повторная инициализация

updated() {
  new AutoNumeric(...)
}

Отсутствие remove()

beforeUnmount() {}

Хранение форматированных значений

"10 000 ₽"

Изменение DOM вручную

input.value = '100';

Необходимо использовать:

an.se t(100);

Архитектурные рекомендации

Лучший подход для Vue

Наиболее стабильная архитектура:

  • отдельный компонент;
  • хранение raw-значения;
  • синхронизация через события;
  • watch с проверкой изменений;
  • удаление экземпляра при unmount;
  • отсутствие прямого v-model на input;
  • динамический import в SSR;
  • composable для переиспользования логики.

Пример production-подхода

<CurrencyInput
  v-model="form.amount"
  :options="currencyOptions"
/>

Внутри компонента:

  • AutoNumeric полностью управляет DOM;
  • Vue управляет состоянием;
  • данные синхронизируются через события;
  • raw-значение хранится отдельно от форматирования.