Поля форм

Восемь полей, которых у платформы нет, — маска, телефон, дата и время, файл с удалением по одному, ввод оценки, одноразовый код, счётчик символов и сводка ошибок, — отдельным файлом слоя GriffinJS. Простой проект не платит за них ни байта; у каждого поля есть база без скрипта.

GriffinJS packages/ui/dist/griffinjs-fields.js packages/ui/src/griffinjs/fields/

Второй бандл, а не рост первого. Поля живут в griffinjs-fields.js и griffinjs-fields.css; в griffinjs.js они не входят ни одним байтом, и его потолок этим разделом не сдвинут. Ядро в бандл полей тоже не входит — он подключается после griffinjs.js или после griffinjs-core.js, как любой модуль слоя. Цена входа — набор «ядро + позиция у якоря + поля»: 13,9 КБ gzip, сами поля — 11,2 КБ, стили — 1,7 КБ. Для сравнения: flatpickr ≈ 15 КБ, imask ≈ 12 КБ, intl-tel-input ≈ 40 КБ с данными — три библиотеки на три пункта из девяти.

Подключение

<link rel="stylesheet" href="griffincss-ui.css">
<link rel="stylesheet" href="griffinjs.css">
<link rel="stylesheet" href="griffinjs-fields.css">
<script src="griffinjs.js"></script>
<script src="griffinjs-fields.js"></script>
<script src="griffinjs-countries.js"></script>   <!-- по желанию: таблица стран для телефона -->

Оба файла слоя на одной странице — правильный сценарий: первый создаёт window.GriffinJS, второй регистрирует в нём поля. Нужны не все поля — берите ядро и файлы по одному, как у остальных модулей; таблица наборов ниже. Порядок тегов свободный, кроме одного: ядро идёт первым. Если чего-то не хватает, слой скажет об этом в консоли при старте.

Догрузка по потребности

Витрине поля нужны на трёх страницах из трёхсот — оформлении заказа, регистрации и отзыве, — а тег бандла стоит в шаблоне шапки для всех. Вместо этого слою можно назвать путь к бандлу, и он возьмёт его сам, когда встретит первое поле: data-gr-mask, data-gr-phone и остальные девять имён загрузчик (griffinjs-loader.js, входит в griffinjs.js; в наборе «ядро + нужное» подключается отдельно) знает как состав файла поставки. Тег вставляется один раз на документ, nonce переносится с тега слоя (под строгим CSP — обязателен, см. «Строгий CSP»), таблица стран, если названа, грузится вторым тегом, и поля поднимаются, когда приехали оба. Страница без полей не грузит ничего.

<!-- слой с defer: пути — атрибутами тега, инлайновый скрипт глобала ещё не видит -->
<script defer src="griffinjs.js"
        data-fields="griffinjs-fields.js"
        data-countries="griffinjs-countries.js"></script>

<!-- слой синхронно или из своего кода — то же самое свойством -->
<script src="griffinjs.js"></script>
<script>
  GriffinJS.config.fields = { src: 'griffinjs-fields.js', countries: 'griffinjs-countries.js' };
</script>
ЧтоКак
Когдапри старте и на каждом узле, который поднимает сканер: поле, вставленное позже, догружает бандл так же
После загрузкивиджеты подняты на всех полях документа; событие griffinjs:fields-loaded на document, в detail.files — что загружено
Ошибка загрузкипредупреждение GriffinJS: бандл полей не загружен, поля остаются базой без скрипта, второй попытки нет
Без configполе без виджета — предупреждение раз на имя: поле «mask» не собрано. Так было бы и раньше, только молча
Стилиgriffinjs-fields.css не догружается: лист в <head> — 1,7 КБ, и он нужен до первой отрисовки поля, а не после
config позже стартаполя проверяются при каждом init(): задали путь после старта — позовите GriffinJS.init()
Набор «ядро + нужное»загрузчик — свой файл griffinjs-loader.js после ядра; атрибуты data-fields ставятся на его тег

Что нового для слоя: поля пишут value

До этого раздела скрипт слоя читал только data-gr-* и писал только состояния — aria-*, data-gr-state, числа вроде --gr-progress. Маска, телефон, дата, сумма и одноразовый код пишут значение контрола: иначе форматировать набранное нельзя. Это расширение инварианта объявлено, а не протащено: у такого поля есть три обязанности. После каждой своей записи оно шлёт то самое событие, которое послала бы платформа, — input, а по уходу фокуса change, — поэтому фреймворк узнаёт о значении так же, как о набранном руками (см. «Оговорка 7» на странице «Griffincss в React и Vue»). Оно никогда не пишет в контрол, на котором не смонтировано. И destroy() возвращает контролу нативный тип, имя и значение.

С griffinjs-fields.js и без

База без скрипта — условие приёмки каждого поля, а не пожелание: страница без скрипта — законное рабочее состояние.

ПолеС griffinjs-fields.jsБаза без JS
Маскалитералы подставляются, каретка встаёт за введённым знаком, подсказка и inputmode из формата<input pattern inputmode> — платформа проверяет сама
Телефонмаска по коду страны, имя страны и флаг от платформы, полный номер с плюсом выбирает странуtype="tel" и <select> кодов в .gr-input-group
Дата и времяна указателе мыши — маска по локали, календарь с годом и временем, ISO в спутнике, проверка границ; уход фокуса возвращает нативное поле; на тач-устройстве и у type="time" — ничегонативный <input type="date">, как сегодня
Диапазон «с — по»значение «с» — min у «по», значение «по» — max у «с»; пустое поле открывает панель на месяце соседа, отрезок между концами помечендва нативных поля с авторскими min/max
Суммаразряды и дробь по локали документа на экране, число в спутнике, каретка держится за цифрой, границы через спутникобычное текстовое поле — сервер получает набранное
Файлсписок выбранного с размером, удаление по одному, накопление выбора<input type="file" multiple> — мультивыбор платформенный
Ввод оценки--gr-rating и слово в aria-label ряда знаков, наведение мышью, подписи поверх знаковпять <input type="radio"> в <fieldset>
Одноразовый кодфокус ходит по ячейкам, Backspace возвращает, вставка и автозаполнение раскладываются, griffin:completeряд ячеек maxlength="1", заполняются вручную
Счётчик символовостаток к maxlength в подписи с aria-live, состояния у порога и на пределеmaxlength ограничивает и так
Сводка ошибоквсе непройденные поля ссылками с сообщениями платформы, фокус на первой, aria-invalidбраузер показывает свой пузырь

Маска

Фундамент раздела: телефон — маска плюс код страны, дата на указателе мыши — маска плюс панель, одноразовый код — маска, разложенная по ячейкам. Формат — строка токенов, не регулярное выражение: из строки получаются и подсказка в пустом поле, и позиция каретки, из regex — ни то, ни другое.

ТокенЧто принимает
0цифра
aбуква, любая по Юникоду
Aбуква, приводится к верхнему регистру
*буква или цифра
?всё после него необязательно — хвост переменной длины
\следующий знак буквально: \0 — литерал «0» (код +380 пишется +38\0)
остальноелитералы; они ленивые — «) » появляется вместе со следующей цифрой
<div class="gr-flex gr-flex-col gr-gap">
  <div class="gr-field">
    <label class="gr-label" for="tel">Телефон</label>
    <input class="gr-input" id="tel" type="tel" pattern="\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}"
           data-gr-mask="+7 (000) 000-00-00">
  </div>
  <div class="gr-field">
    <label class="gr-label" for="at">Время</label>
    <!-- формат с двоеточием или параметры — парами -->
    <input class="gr-input" id="at" type="text" data-gr-mask="format: 00:00; hint: false">
  </div>
</div>
Параметр data-gr-maskПо умолчаниюЧто делает
formatсам атрибутФормат; без ключа атрибут читается целиком как формат
hinttrueПодсказка пустого поля из формата — если автор не задал placeholder
fill_Знак свободного слота в подсказке
incomplete«Значение набрано не полностью»Сообщение о недоборе: непустое значение с незаполненными обязательными слотами не проходит проверку формы

Каретка: правки перехватываются в beforeinput, значение пишется целиком, каретка встаёт за только что введённым знаком; Backspace и Delete перешагивают литералы и снимают именно знак. На всём, чего маска не понимает — композиция IME и экранной клавиатуры Android, автозаполнение, автозамена, отмена, — она отступает, а не гадает: браузер делает своё, а маска потом приводит значение к формату. Состояние в data-gr-state: empty, partial, complete.

Недобор не проходит проверку. Непустое значение с незаполненными обязательными слотами — своя ошибка поля: checkValidity() даёт false, и форма с половиной номера не отправляется. Красным (aria-invalid) поле помечается только по уходу фокуса и светлеет сразу, как только значение стало полным: ругаться на человека, пока он печатает, — не проверка. Пустое поле маска не трогает — пустое это required платформы, и поле без него обязательным не становится. На полном значении своё сообщение снимается, и единственным судьёй снова становится авторский pattern: его маска не заменяет. Там, где недобор законен, выход не требует нового параметра — токен ? в формате делает хвост необязательным (номерной знак ниже полон и без региона). destroy() возвращает поле в валидное состояние.

Телефон, номерной знак и время нужен griffinjs-fields.js

Телефон

Маска плюс код страны, и ничего сверх того. Состав стран виджету не принадлежит: он читает его из разметки — value кода, data-gr-iso и data-gr-format на каждой позиции <select>. Имя страны и флаг — у платформы: Intl.DisplayNames в языке страницы и пара региональных индикаторов из двух букв ISO. Ни строки на перевод, ни картинки.

<div class="gr-input-group" data-gr-phone>
  <select class="gr-select" name="code" aria-label="Код страны">
    <option value="+7" data-gr-iso="RU" data-gr-format="(000) 000-00-00" selected>RU +7</option>
    <option value="+375" data-gr-iso="BY" data-gr-format="(00) 000-00-00">BY +375</option>
  </select>
  <input class="gr-input" type="tel" name="phone" autocomplete="tel-national">
</div>

Пустой <select> заполняется из таблицы griffinjs-countries.js0,9 КБ gzip, 86 стран, ISO2, код и маска, отсортированные по имени в языке страницы; country: kz выбирает начальную. Таблица — отдельный файл по той же причине, что и слот под иконку: набор данных со своим темпом обновления библиотека вместе с кодом не сопровождает. Своя позиция в разметке сильнее таблицы, без таблицы и без позиций виджет предупреждает и работает как поле без маски.

Параметр data-gr-phoneПо умолчаниюЧто делает
countryISO начальной страны, когда список заполняется из таблицы. Без него список начинается с первой страны по алфавиту языка страницы — на русской это Австралия, +61, — и виджет предупреждает об этом в консоли; авторских <option> предупреждение не касается
namestrueПереписывать подписи позиций в «🇷🇺 Россия +7»; false оставляет авторские
Атрибут <option>Что несёт
valueКод страны с плюсом — уходит в форму
data-gr-isoДве буквы ISO: из них имя страны и флаг
data-gr-formatМаска национального номера — токенами, как у маски
data-gr-trunkНациональный префикс, если он не ноль: 8 у России и Казахстана. Номер на знак длиннее маски, начатый с него, теряет префикс — так «8 912 …» становится «+7 912 …», а десять знаков с восьмёрки остаются номером

Смена кода меняет маску, знаки номера остаются. Полный номер с плюсом — вставка из буфера или автозаполнение — выбирает страну по самому длинному подходящему коду. Форма отправляет два поля: код и национальный номер; целиком он в value() экземпляра и в событии griffin:change (iso, code, value, complete).

Что проверяется

Проверяется длина номера по маске выбранной страны. Недобранный номер не проходит checkValidity() — проверка приходит от маски и работает у телефона так же, как у ИНН и номерного знака. Смена страны пересчитывает полноту: девяти знаков хватает Беларуси и мало России, и полное значение от смены кода битым не становится. Красным поле становится по уходу фокуса.

Чего мы не обещаем. Тип номера и диапазон оператора не проверяются: libphonenumber в замену не входит — ровно поэтому набор с полями весит килобайты, а не сорок. «Телефон набран до конца» и «такой номер существует» — разные утверждения, и первое здесь не выдаётся за второе. Нужна вторая — берите библиотеку с данными операторов и оставьте телефон полю ввода.

value() при этом отражает набранное, даже неполное: это его работа — показывать, что в поле. Полнота уезжает отдельным признаком complete в событии griffin:change и видна в checkValidity() самого поля. Где недобор законен, выход тот же, что у маски, — токен ? в data-gr-format страны.

Своя таблица стран

Список кодов — данные, и они ваши. Три способа задать состав, от самого простого: перечислить позиции в разметке; подключить готовую таблицу griffinjs-countries.js; положить свою таблицу в GriffinJS.phone.table до старта слоя — в том же формате, что у файла: ISO2, код, маска и, где он есть, национальный префикс. Так выглядит сам файл; заменить его своим — значит повторить эту форму.

// griffinjs-countries.js — так устроен поставляемый файл (фрагмент)
var TABLE = [
  ['RU', '+7',   '(000) 000-00-00', '8'],
  ['KZ', '+7',   '(000) 000-00-00', '8'],
  ['BY', '+375', '(00) 000-00-00'],
  ['UA', '+380', '(00) 000-00-00'],
  ['US', '+1',   '(000) 000-0000'],
  ['DE', '+49',  '000 0000000?0000'],
  ['GB', '+44',  '0000 000000'],
  …
];

// Своя таблица вместо файла: до старта слоя, тем же составом строки
<script src="griffinjs.js" data-auto="false"></script>
<script src="griffinjs-fields.js"></script>
<script>
  GriffinJS.phone.table = [
    ['RU', '+7',   '(000) 000-00-00', '8'],
    ['BY', '+375', '(00) 000-00-00'],
    ['KZ', '+7',   '(000) 000-00-00', '8'],
  ];
  GriffinJS.start();
</script>

<!-- Или те же строки разметкой — сильнее любой таблицы -->
<select class="gr-select" name="code" aria-label="Код страны">
  <option value="+7"   data-gr-iso="RU" data-gr-format="(000) 000-00-00" data-gr-trunk="8" selected>RU +7</option>
  <option value="+375" data-gr-iso="BY" data-gr-format="(00) 000-00-00">BY +375</option>
  <option value="+7"   data-gr-iso="KZ" data-gr-format="(000) 000-00-00" data-gr-trunk="8">KZ +7</option>
</select>

Имя страны и флаг виджет достроит сам по ISO — в языке страницы, поэтому в таблице их нет. Полный список поставляемого файла — 86 стран: всё постсоветское пространство, Европа, крупные страны Азии, Америки и Африки; исходник — packages/ui/src/griffinjs/fields/countries.js.

Состав из таблицы, начальная страна — Казахстан нужен griffinjs-fields.js

Дата и время

Ответ по умолчанию — прежний: нативное <input type="date"> в оформлении семьи .gr-field, раздел «Выбор даты и времени» на странице форм. Разметка всегда несёт нативное поле, и без скрипта форма работает ровно как сегодня. Виджет не спорит с платформой, а делит с ней работу по pointerType каждого события: на тач-устройстве он разметку не трогает вовсе — системный календарь лучше нашего; на указателе мыши тип подменяется на text, надевается маска по локали из lang документа, значение уходит в скрытый спутник с прежним именем в ISO, а показывается текст через Intl.DateTimeFormat — то, чего нативное поле не умеет. Подмена живёт, пока поле в работе: уход фокуса возвращает нативный тип — значок календаря и подсказку платформы, — а следующий указатель мыши подменяет снова.

<div class="gr-flex gr-flex-col gr-gap">
  <div class="gr-field">
    <label class="gr-label" for="from">Дата</label>
    <input class="gr-input" id="from" type="date" name="from" min="2026-01-01" max="2026-12-31" required data-gr-datetime>
  </div>
  <div class="gr-field">
    <label class="gr-label" for="at">Время</label>
    <input class="gr-input" id="at" type="time" name="at">   <!-- нативное: выбор встроен в поле -->
  </div>
  <div class="gr-field">
    <label class="gr-label" for="when">Дата и время</label>
    <input class="gr-input" id="when" type="datetime-local" name="when" data-gr-datetime>
  </div>
</div>

Плата. На текстовом поле платформа не проверяет min и max — проверка переезжает в виджет через setCustomValidity. Спутник несёт границы, и там, где платформа умеет, сообщение о них берётся у неё, в языке браузера; о невозможной дате — своё, из параметра invalid. required остаётся на видимом поле и проверяется платформой как прежде. Форма отправляет ISO под прежним именем, как без скрипта. destroy() возвращает нативный тип.

Параметр data-gr-datetimeПо умолчаниюЧто делает
invalidТакой даты нетСообщение о невозможном значении — «31.02.2026», «15.09.2026, 25:70». Набранное целиком помечается сразу: aria-invalid на поле, неполный ввод не краснеет
rangeДата вне допустимых границСообщение о границах там, где у платформы своего нет
label, prev, next, year, timeКалендарь, Предыдущий месяц, Следующий месяц, Год, ВремяПодписи панели, её стрелок, списка лет и поля времени
toСелектор поля «по»: пара «с — по», см. ниже

Панель

У даты и даты-времени — сетка месяца, role="dialog", фокус остаётся в поле, активный день — aria-activedescendant, год — отдельным списком в шапке (от нижней границы или ста двадцати лет назад до верхней или двадцати вперёд: дата рождения и бронирование одним списком). Первый день недели — у платформы (Intl.Locale.weekInfo), положение у поля — G.anchor.place, как у дропдауна. У даты-времени под сеткой стоит время — нативное <input type="time">, которое проверяет платформа; в маске оно набирается тоже, всегда в 24-часовом виде.

Поле времени (type="time") виджет не трогает вовсе: выбор часов и минут у платформы встроен в само поле, и подменять его текстом с панелью значило бы отдавать при фокусе дубль того же поля. Атрибут на нём допустим — одна разметка годится всем трём типам, — но делает он там ничего.

КлавишаДействие
Неделя вперёд и назад; на закрытой панели открывает её
День — пока день активен; до этого ходят по тексту в поле
Home EndКрай недели — тоже при активном дне
PageUp PageDownМесяц; с Shift — год
EnterВыбрать активный день
EscЗакрыть панель, фокус на месте
Дата с границами, дата и время; время — нативное нужен griffinjs-fields.js
В пределах 2026 года. На телефоне откроется системный календарь.

Диапазон «с — по»

Пара нативных полей, связанная атрибутом to на первом. Одного контрола с двумя концами нет намеренно: база без скрипта — два нативных поля, и панель одного поля пишет одно поле. Связка строится на готовом: значение «с» становится min у «по», значение «по» — max у «с», через собственный API соседа, — и проверка границ, сообщения и недоступные дни в панели работают без новой логики; на тач-устройстве системный календарь сам скрывает недоступное. Авторская граница остаётся, если она уже; очистка и destroy() возвращают её. Порядок дат виджет не толкает: «по» раньше «с» — ошибка ввода, и её показывает проверка границ, а не молчаливая правка второго поля.

<input class="gr-input" id="since" type="date" name="since" data-gr-datetime="to: #until">
<input class="gr-input" id="until" type="date" name="until" data-gr-datetime>

Сверх границ пара добавляет только панель: пустое поле открывается на месяце соседа, оба конца — aria-selected, дни между ними — data-gr-in-range (красит CSS панели), а в griffin:change обоих полей приходит detail.range = {from, to}.

Заезд и выезд: выезд не раньше заезда, отрезок в обеих панелях нужен griffinjs-fields.js

Чего здесь не будет: диапазона одним контролом с двумя концами, номеров недель, часовых поясов и повторяющихся событий. Каждый пункт выглядит на полчаса, и каждый заводит то, чего у поля нет: модель состояния. Предел записан в плане и проверяется обзором.

Сумма

Отдельное поле, а не маска: type="number" разрядов не показывает, inputmode не форматирует, а маска знает только фиксированные слоты — переменной длины и разрядов у неё нет. На экране — текст с разделителями по локали документа («1 250 000,50» при lang="ru", «1,250,000.50» при lang="en"; без lang — язык браузера), число в точечной записи (1250000.50) — в скрытом спутнике type="number" с прежним именем: тот же приём, что у даты. Разряды встают на ходу, каретка держится за той же цифрой, Backspace и Delete перешагивают разделители. Валюта и единица — не здесь: .gr-input-addon рядом.

<div class="gr-input-group">
  <input class="gr-input" type="text" inputmode="decimal" name="amount"
         min="0" max="10000000" data-gr-number="decimals: 2">
  <span class="gr-input-addon">₽</span>
</div>

Разбор принимает любую запись — набранную и вставленную из буфера. Разделитель дроби — последний из «,» и «.», если это знак дроби локали или чужой знак не с тремя цифрами за ним: «1 250 000,50» и «1250000.50» читаются одинаково, «1.250.000» в ru — разряды, «1,250,000» в en — тоже. Всё, кроме цифр и минуса в начале, отбрасывается; минус принимается, только если min не задан или отрицателен. Лишние цифры дроби отбрасываются, не округляются: сумма — то, что набрано. Дробь дополняется нулями до decimals по уходу фокуса; спутник несёт число как есть.

Плата названа. База без скрипта — обычное текстовое поле: без бандла полей сервер получит то, что набрано, с пробелами и запятой. Разбор на сервере обязателен в любом случае — и со скриптом тоже: значение спутника приходит с клиента, как всякое другое. Границы min/max переезжают в спутник, и там, где платформа умеет, сообщение о выходе за них — её, в языке браузера; иначе — из параметра range. Красным (aria-invalid) поле помечается по уходу фокуса и светлеет, как только значение вернулось в границы. Пустое поле — required платформы, не виджета.

Параметр data-gr-numberПо умолчаниюЧто делает
decimals0Знаков дроби; при нуле дробь отбрасывается, набранная точка отвергается, как буква
rangeЗначение вне допустимых границСообщение о границах там, где у платформы своего нет
min, max, stepс поляНе параметры, а атрибуты поля: уходят в спутник, проверяет платформа; без stepany
Сумма с дробью и ставка без неё; число — в форму нужен griffinjs-fields.js
%

API экземпляра: value() — число как в спутнике, set(value) — записать число (шлёт input), destroy() — вернуть имя и число в поле. Событие griffin:change с detail.value — на каждое новое число; о значении из разметки не сообщается.

Файл

Модуль .gr-file пакета ui — поле семьи: рамка, фокус и состояния те же, что у .gr-input, кнопка выбора — ::file-selector-button. Мультивыбор даёт атрибут multiple, и без скрипта поле показывает число выбранных само. Виджет нужен ради одного: убрать один файл из выбранного — ни разметкой, ни CSS этого не сделать, input.files переписывается только через DataTransfer.

<div class="gr-flex gr-flex-col gr-gap">
  <div class="gr-field">
    <label class="gr-label" for="docs">Документы</label>
    <input class="gr-file" id="docs" type="file" name="docs" multiple data-gr-file>
  </div>
  <div class="gr-field">
    <label class="gr-label" for="photos">Фотографии</label>
    <input class="gr-file" id="photos" type="file" name="photos" multiple accept="image/*" data-gr-file="append">
  </div>
</div>
Параметр data-gr-fileПо умолчаниюЧто делает
appendfalseПовторный выбор добавляет к прежнему без повторов, а не заменяет его
dropfalseРодитель поля — зона перетаскивания: брошенные на него файлы ложатся в поле, над ним — состояние over
listСвой контейнер списка (#id); без него список создаётся сразу после поля
removeУбрать {name}Подпись кнопки «убрать», для скринридера

Зона перетаскивания

Обёртка .gr-file-drop вокруг того же поля: пунктирная область с центрированным содержимым — тот же приём, что центрирование одним классом. Без скрипта это крупная кнопка выбора: щелчок по подписи открывает выбор файла, а бросить файл можно на само поле — это умеет платформа. Со скриптом и флагом drop файлы принимает вся зона, а пока их тянут над ней, она подсвечивается.

<label class="gr-file-drop">
  <input class="gr-file" type="file" name="docs" multiple data-gr-file="drop; append">
  Перетащите файлы сюда или выберите
</label>
Зона принимает файлы и подсвечивается над ними нужен griffinjs-fields.js

Размер — в единицах локали через Intl.NumberFormat (2 кБ, 3 МБ), ни строки на перевод. Список — aria-live="polite"; после удаления фокус уходит на соседнюю кнопку, а без неё — на поле. Событие griffin:change несёт массив файлов. Загрузка на сервер, прогресс и превью — не здесь: сеть и политика не сводятся к разметке.

Замена и накопление выбора нужен griffinjs-fields.js

Ввод оценки

Модуль .gr-rating сам объявил ввод оценки работой слоя: CSS рисует ряд по числу в --gr-rating, а откуда число — не его дело. Здесь оно берётся из выбранной радиокнопки. База без скрипта — пять <input type="radio"> в <fieldset>: выбор, клавиатура и участие в форме платформенные. Со скриптом CSS слоя кладёт подписи поверх ряда знаков (состояние ready), число уходит в --gr-rating, подпись выбранной — в aria-label ряда, наведение мышью показывает оценку под указателем.

<fieldset class="gr-rating-input" data-gr-rating>
  <legend class="gr-label">Оценка</legend>
  <span class="gr-rating" role="img" aria-label="Без оценки"></span>
  <label><input type="radio" name="score" value="1"> 1 из 5</label>
  …
  <label><input type="radio" name="score" value="5"> 5 из 5</label>
</fieldset>
Оценка товара; стрелки клавиатуры меняют выбор нужен griffinjs-fields.js
Оценка

Одноразовый код

Маска, разложенная по ячейкам. База — ряд ячеек maxlength="1": заполняются вручную, каждая уходит в форму своим значением. Скрипт добавляет то, чего платформа сама не делает: набранный знак переводит фокус в следующую ячейку, Backspace на пустой возвращает в предыдущую и чистит её, стрелки ходят по ряду, а код из буфера или из автозаполнения (autocomplete="one-time-code") — целиком в одну ячейку — раскладывается по всем, начиная с неё. Полный код — событие griffin:complete со значением. Цифровые ячейки (inputmode="numeric") принимают только цифры.

<div class="gr-otp" role="group" aria-label="Код из СМС" data-gr-otp>
  <input class="gr-input gr-otp-cell" type="text" inputmode="numeric" maxlength="1"
         name="code[]" autocomplete="one-time-code" aria-label="Знак 1">
  … ещё пять
</div>
Шесть цифр; вставьте код целиком в любую ячейку нужен griffinjs-fields.js
Код из СМС

Счётчик символов

Предел задаёт maxlength, и без скрипта он действует сам. Скрипт добавляет только цифру — сколько ещё можно ввести: пишет её в <output> сразу после поля (или в узел из out), связывает с полем через aria-describedby и объявляет скринридеру через aria-live. У порога подпись получает состояние near, на пределе — full; красит по ним CSS.

<div class="gr-flex gr-flex-col gr-gap">
  <div class="gr-field">
    <label class="gr-label" for="about">О себе</label>
    <textarea class="gr-textarea" id="about" maxlength="200" data-gr-counter></textarea>
  </div>
  <div class="gr-field">
    <label class="gr-label" for="note">Примечание</label>
    <textarea class="gr-textarea" id="note" maxlength="200" data-gr-counter="format: {left} из {max}; near: 0.2; out: #left"></textarea>
    <span class="gr-hint" id="left"></span>
  </div>
</div>
Параметр data-gr-counterПо умолчаниюЧто делает
format{left}Шаблон подписи: {left} — остаток, {length} — введено, {max} — предел
near0.1Доля предела, ниже которой остаток «на исходе»
outСвой узел подписи (#id)
maxlength=40, порог 25 % нужен griffinjs-fields.js

Сводка ошибок

Без скрипта форму проверяет браузер и показывает свой пузырь у первого непройденного поля — одного, по одному за раз. Скрипт собирает все непройденные поля в одну сводку в начале формы: ссылки «подпись: сообщение», где сообщение — платформенное, в языке браузера, а подпись — <label> поля, иначе aria-label или name. Фокус после отправки — на первой ссылке, Enter по ней ведёт к полю. Непройденные поля получают aria-invalid="true" — источник для программы чтения с экрана, — а их обёртка .gr-field, если она есть, состояние data-gr-state="error": его и красит CSS семьи. Оба снимаются, как только поле исправлено (обёртка с двумя контролами держит состояние, пока не пройден хотя бы один); список обновляется следующей отправкой. Пузырь браузера на время жизни виджета снимается (novalidate), проверку это не отменяет: checkValidity() платформы остаётся единственным судьёй.

<form data-gr-validate>
  …поля с required, pattern, min, max…
  <button class="gr-btn gr-btn-primary" type="submit">Отправить</button>
</form>
Параметр data-gr-validateПо умолчаниюЧто делает
summaryСвой контейнер сводки (#id); без него блок .gr-validate создаётся первым узлом формы
titleФорма не отправлена — проверьте поля:Заголовок над списком
Три обязательных поля; нажмите «Отправить» пустыми нужен griffinjs-fields.js

Модульная сборка

Кроме griffinjs-fields.js целиком в dist/ лежат файлы по одному на поле — griffinjs-mask.js, griffinjs-phone.js и так далее. Ядро — griffinjs-core.js или весь griffinjs.js — идёт первым; что поле берёт из слоя, оно объявляет само, и таблица собрана из этих объявлений при сборке.

Все поля одним файлом — 11,2 КБ gzip без ядра; набор «ядро + anchor + поля» — 13,9 КБ. По одному поля берутся так же, как виджеты: ядро первым, затем объявленное и само поле. Таблица собрана из объявлений при сборке.

ПолеПодключить после griffinjs-core.jsНабор целиком
data-gr-maskgriffinjs-mask.js4,0 КБ
data-gr-phonegriffinjs-mask.js + griffinjs-phone.js5,4 КБ
data-gr-datetimegriffinjs-anchor.js + griffinjs-mask.js + griffinjs-datetime.js8,6 КБ
data-gr-numbergriffinjs-number.js4,3 КБ
data-gr-filegriffinjs-file.js3,9 КБ
data-gr-ratinggriffinjs-rating.js3,2 КБ
data-gr-otpgriffinjs-otp.js3,5 КБ
data-gr-countergriffinjs-counter.js3,3 КБ
data-gr-validategriffinjs-validate.js3,8 КБ

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

Поля собираются в формы вместе с остальной семьёй .gr-field. Три сцены, где второй бандл окупает себя: регистрация, подтверждение кодом и отзыв о товаре.

Регистрация: телефон, дата рождения и сводка ошибок

Телефон из таблицы стран, дата с верхней границей, обязательные поля и одна сводка вместо пузырей по одному. Без скрипта всё то же — кодом в списке, системным календарём и пузырём браузера.

<form data-gr-validate>
  <div class="gr-field">
    <label class="gr-label" for="tel">Телефон</label>
    <div class="gr-input-group" data-gr-phone="country: ru">
      <select class="gr-select" name="code" aria-label="Код страны"></select>
      <input class="gr-input" id="tel" type="tel" name="phone" required autocomplete="tel-national">
    </div>
  </div>
  <div class="gr-field">
    <label class="gr-label" for="born">Дата рождения</label>
    <input class="gr-input" id="born" type="date" name="born" max="2008-09-02" required data-gr-datetime>
  </div>
  <button class="gr-btn gr-btn-primary" type="submit">Создать аккаунт</button>
</form>
Регистрация нужен griffinjs-fields.js
Регистрация с 18 лет.

Подтверждение кодом из СМС

Ряд ячеек с autocomplete="one-time-code" на первой: на телефоне код подставит платформа, на компьютере его вставят из буфера — и в обоих случаях он разложится по ячейкам. Полный код — событие, по которому форма уходит сама.

<form action="/confirm" method="post">
  <div class="gr-otp" role="group" aria-label="Код из СМС" data-gr-otp>…шесть ячеек…</div>
</form>
<script>
  document.querySelector('[data-gr-otp]').addEventListener('griffin:complete', function (e) {
    e.target.closest('form').requestSubmit();
  });
</script>

Отзыв о товаре: оценка, текст с лимитом и фотографии

Оценка звёздами поверх радиокнопок, текст с остатком к пределу и фотографии, которые докладывают по одной. Всё три поля — обычные контролы формы: без скрипта отзыв отправится ровно так же.

Отзыв нужен griffinjs-fields.js
Оценка