Поля форм
Восемь полей, которых у платформы нет, — маска, телефон, дата и время, файл с удалением по одному, ввод оценки, одноразовый код, счётчик символов и сводка ошибок, — отдельным файлом слоя GriffinJS. Простой проект не платит за них ни байта; у каждого поля есть база без скрипта.
Второй бандл, а не рост первого. Поля живут
в 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 | сам атрибут | Формат; без ключа атрибут читается целиком как формат |
hint | true | Подсказка пустого поля из формата — если автор не задал placeholder |
fill | _ | Знак свободного слота в подсказке |
incomplete | «Значение набрано не полностью» | Сообщение о недоборе: непустое значение с незаполненными обязательными слотами не проходит проверку формы |
Каретка: правки перехватываются в beforeinput, значение
пишется целиком, каретка встаёт за только что введённым знаком; Backspace
и Delete перешагивают литералы и снимают именно знак. На всём, чего маска
не понимает — композиция IME и экранной клавиатуры Android,
автозаполнение, автозамена, отмена, — она отступает, а не
гадает: браузер делает своё, а маска потом приводит значение
к формату. Состояние в data-gr-state: empty,
partial, complete.
Недобор не проходит проверку. Непустое значение с незаполненными
обязательными слотами — своя ошибка поля: checkValidity()
даёт false, и форма с половиной номера не отправляется.
Красным (aria-invalid) поле помечается только
по уходу фокуса и светлеет сразу, как только значение
стало полным: ругаться на человека, пока он печатает, — не проверка.
Пустое поле маска не трогает — пустое это required
платформы, и поле без него обязательным не становится. На полном
значении своё сообщение снимается, и единственным судьёй снова
становится авторский pattern: его маска не заменяет.
Там, где недобор законен, выход не требует нового параметра — токен
? в формате делает хвост необязательным (номерной знак
ниже полон и без региона). destroy() возвращает поле
в валидное состояние.
Телефон
Маска плюс код страны, и ничего сверх того. Состав стран виджету
не принадлежит: он читает его из разметки — 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.js — 0,9 КБ gzip,
86 стран, ISO2, код и маска, отсортированные по имени в языке страницы;
country: kz выбирает начальную. Таблица — отдельный файл
по той же причине, что и слот под иконку: набор данных со своим темпом
обновления библиотека вместе с кодом не сопровождает. Своя позиция
в разметке сильнее таблицы, без таблицы и без позиций виджет
предупреждает и работает как поле без маски.
Параметр data-gr-phone | По умолчанию | Что делает |
|---|---|---|
country | — | ISO начальной страны, когда список заполняется из таблицы. Без него список начинается с первой страны по алфавиту языка страницы — на русской это Австралия, +61, — и виджет предупреждает об этом в консоли; авторских <option> предупреждение не касается |
names | true | Переписывать подписи позиций в «🇷🇺 Россия +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.
Дата и время
Ответ по умолчанию — прежний: нативное <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 | Закрыть панель, фокус на месте |
Диапазон «с — по»
Пара нативных полей, связанная атрибутом 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}.
Чего здесь не будет: диапазона одним контролом с двумя концами, номеров недель, часовых поясов и повторяющихся событий. Каждый пункт выглядит на полчаса, и каждый заводит то, чего у поля нет: модель состояния. Предел записан в плане и проверяется обзором.
Сумма
Отдельное поле, а не маска: 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 | По умолчанию | Что делает |
|---|---|---|
decimals | 0 | Знаков дроби; при нуле дробь отбрасывается, набранная точка отвергается, как буква |
range | Значение вне допустимых границ | Сообщение о границах там, где у платформы своего нет |
min, max, step | с поля | Не параметры, а атрибуты поля: уходят в спутник, проверяет платформа; без step — any |
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 | По умолчанию | Что делает |
|---|---|---|
append | false | Повторный выбор добавляет к прежнему без повторов, а не заменяет его |
drop | false | Родитель поля — зона перетаскивания: брошенные на него файлы ложатся в поле, над ним — состояние 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>
Размер — в единицах локали через Intl.NumberFormat
(2 кБ, 3 МБ), ни строки на перевод. Список —
aria-live="polite"; после удаления фокус уходит на соседнюю
кнопку, а без неё — на поле. Событие griffin:change несёт
массив файлов. Загрузка на сервер, прогресс и превью — не здесь: сеть
и политика не сводятся к разметке.
Ввод оценки
Модуль .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>
Одноразовый код
Маска, разложенная по ячейкам. База — ряд ячеек 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>
Счётчик символов
Предел задаёт 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} — предел |
near | 0.1 | Доля предела, ниже которой остаток «на исходе» |
out | — | Свой узел подписи (#id) |
Сводка ошибок
Без скрипта форму проверяет браузер и показывает свой пузырь у первого
непройденного поля — одного, по одному за раз. Скрипт собирает
все непройденные поля в одну сводку в начале формы:
ссылки «подпись: сообщение», где сообщение — платформенное, в языке
браузера, а подпись — <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 целиком в dist/ лежат
файлы по одному на поле — griffinjs-mask.js,
griffinjs-phone.js и так далее. Ядро — griffinjs-core.js
или весь griffinjs.js — идёт первым; что поле берёт
из слоя, оно объявляет само, и таблица собрана из этих объявлений
при сборке.
Все поля одним файлом — 11,2 КБ gzip без ядра; набор «ядро + anchor + поля» — 13,9 КБ. По одному поля берутся так же, как виджеты: ядро первым, затем объявленное и само поле. Таблица собрана из объявлений при сборке.
| Поле | Подключить после griffinjs-core.js | Набор целиком |
|---|---|---|
data-gr-mask | griffinjs-mask.js | 4,0 КБ |
data-gr-phone | griffinjs-mask.js + griffinjs-phone.js | 5,4 КБ |
data-gr-datetime | griffinjs-anchor.js + griffinjs-mask.js + griffinjs-datetime.js | 8,6 КБ |
data-gr-number | griffinjs-number.js | 4,3 КБ |
data-gr-file | griffinjs-file.js | 3,9 КБ |
data-gr-rating | griffinjs-rating.js | 3,2 КБ |
data-gr-otp | griffinjs-otp.js | 3,5 КБ |
data-gr-counter | griffinjs-counter.js | 3,3 КБ |
data-gr-validate | griffinjs-validate.js | 3,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>
Подтверждение кодом из СМС
Ряд ячеек с 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>
Отзыв о товаре: оценка, текст с лимитом и фотографии
Оценка звёздами поверх радиокнопок, текст с остатком к пределу и фотографии, которые докладывают по одной. Всё три поля — обычные контролы формы: без скрипта отзыв отправится ровно так же.