Всем привет!


Я только что выпустил релиз шестой версии библиотеки imaskjs. После каждого мажорного релиза мне кажется, что это последняя версия. Библиотеке уже не один год, она стабильна и достаточно популярна среди сообщества. Что там еще можно сделать да еще и на мажорную версию? Во всех своих проектах я стараюсь делать задачи так, чтобы больше к ним не возвращаться в рамках текущего контекста. Что может измениться в маске? — Браузеры не меняют свои API, javascript все такой же, бизнес задачи все те же — телефоны, карты, числа. Почему нельзя уже остановиться, довести до ума и больше не трогать?


Периодически конечно надо подпушивать, чтобы казалось что проект жив и обновлялась дата последнего коммита на github. Не я один же на нее смотрю, когда выбираю библиотеку?


Но серьезных оснований для изменений вроде бы нет. На самом деле есть. В моем случае с маской основные изменения касаются экосистемы javascript: сам язык давно вышел за пределы браузера и распространяется туда, где раньше использовались нативные инструменты, что накладывает новые требования к инструментам. Также появляются новые UI-фреймворки, требующие некоторой адаптации. Хотя мне кажется интерфейс библиотеки сделан максимально просто и удобно, поэтому нет и не может быть проблем с интеграцией вообще ни с чем, во всяком случае в браузере. Но тут я взялся за работу над плагином для React Native.


Лезем в трубу на React Native


Насколько я знаю, React Native — это масштабный проект, который живет уже очень давно, и серьезные люди даже использует в проде. Пару лет назад я попробовал использовать RN на одном небольшом тестовом проекте, и на этом моя практика закончилась — я ушел в веб. Приблизительно в то же самое время, я сделал плагин для маски на RN и долго и упорно пытался заставить его работать как мне бы хотелось, но так и не смог. Я нашел несколько багов в репозитории RN на тему обработки ввода и позиционирования курсора, успокоился и решил немного подождать.


Прошло больше года и я подумал, что пришло время вернуться и уже разделаться с плагином. Оказалось, что за это время не изменилось практически ничего: старые баги были закрыты из-за отсутствия активности, немного изменился интерфейс позиционирования курсора ну и собственно все. Мне бы хотелось думать, что проблема в моей библиотеке, я все поправлю, и заработает. Увы, на мой взгляд маска уже имеет максимально простой интерфейс — большая часть логики вынесена в ядро, для обработки ввода нужно всего лишь два состояния — курсор и значение до изменения и после. По-моему проще некуда, но мне так и не удалось завести RN.


Одна из сложностей заключается в том, что последовательность событий ввода в RN отличается от браузера. Например в браузере на событие input курсор уже находится в новой позиции, но в RN существует отдельное событие onSelectionChange, которое возникает после изменения значения, причем только на iOS, на Android наоборот. И второй момент — на RN нельзя менять значение в поле в обработчике события изменения значения, то же самое с позиционированием курсора. Поэтому неизбежны задержки в изменении, и как следствие дерганье текста и курсора. Этим багам уже почти три года, поэтому не вижу смысла больше ждать — выкатил плагин как есть, но использовать не советую. Скорее сделал для привлечения внимания общества: может баги поправят в RN, а может кто-то подключит к IMask обработку событий в нативном коде, где таких проблем можно избежать.


Своя труба


Теперь немного о хорошем. Маску теперь можно использовать для форматирования и конвертирования:


const numberPipe = IMask.createPipe({
  mask: Number,
  scale: 2,
  thousandsSeparator: ' ',
  normalizeZeros: true,
  padFractionalZeros: true,
});
// `numberPipe` - это обычная функция
numberPipe('1'); // "1,00"

Можно переиспользовать одну маску и для работы с вводом и для форматирования, например на форме:


// используем на элементе формы
const mask = IMask(el, {
  mask: Number,
  scale: 2,
  thousandsSeparator: ' ',
  normalizeZeros: true,
  padFractionalZeros: true,
});

// можно использовать pipe напрямую для конвертации значения,
// если не будет переиспользована
IMask.pipe(
  1,  // значение
  mask.masked, // достаем маску из обертки
  IMask.PIPE_TYPE.TYPED,  // опционально формат входного значения
  IMask.PIPE_TYPE.UNMASKED // опционально формат выходного значения
)
// 1.00

В Angular плагине для удобства обернул в местный Pipe.
Опять же максимально просто и функционально, реализация буквально в 5 строк кода — это к слову как показатель удачной архитектуры. Тем не менее по 5 строк кода добавляется уже не первый раз и размерчик всей библиотеки набежал уже серьезный.


ESM модули


Пару лет назад когда маска была около 30 Кб я всем говорил, что для такой маски это немного. Сейчас она уже почти 60 Кб и даже мне показалось многовато. Есть одно простое решение — gzip брать только то, что нужно. В идеале сборщик должен автоматически проанализировать зависимости и выбросить все что не используется. Хотя ESM модули есть в маске уже давно, но treeshaking почти не работал, т.к. ссылки на внутренние сущности находились в корневом объекте IMask. Это было сделано из-за присутствия циклических зависимостей внутри маски, а еще, потому что мне хотелось чтобы маску можно было импортировать как один корневой объект, так и отдельно по частям. Например так сделано в React:


import { Component } from 'react';
import React from 'react';
React.Component === Component; // true

В IMask 6.0 жесткая связь между модулями была разорвана и появилась возможность импортировать отдельные модули:


// вместо старого подхода
// import IMask from 'imask'; // импорт фабрики и всех модулей

import IMask from 'imask/esm/imask'; // импорт только фабрики, без модулей
// подключение необходимых модулей
import 'imask/esm/masked/number';
// теперь маска будет понимать числовые маски, но никакие другие доступны не будут
const numberMask = IMask(element, { mask: Number });

При этом можно продолжать использовать старый подход и ничего не сломается, но и treeshaking работать не будет.


Ну и на закуску: в новой версии появилась поддержка contenteditable и web-компонентов! Спасибо комьюнити за обратную связь и пул реквесты.


С каждым новым релизом баги становятся все тоньше, пожелания все экзотичнее. Вот опять кажется дело движется к концу. И так похоже будет бесконечно, пока проект не умрет или не появится что-то лучшее. А может платформа поменяется и появятся новые интерфейсы. Но судя по всему сейчас браузеры находятся где-то между молодостью и зрелостью, а после смерти text-mask, альтернатив для IMask по функционалу не видно, да и я еще могу найти каплю времени на open source, поэтому маске быть.


Всем успехов и вдохновения в Новом Году, с Наступающим!

Комментарии (1)


  1. DimNS
    26.12.2019 11:34

    Вот уже какой год пользуюсь cleave.js (количество звезд впечатляет), о вашем проекте узнал только что.

    В cleave.js очень не хватает возможности лимитировать ввода min/max (есть issue в котором люди продолжают ставить +1, но авторы не торопятся внедрять эту фичу), у вас я смотрю такая фича реализована и это здорово