Формы: ввод, проверка и суммы
- Зачем это нужно
- Состояние формы — один объект
- Поле «число» отдаёт не число
- Что браузер проверяет бесплатно
- Когда показывать ошибку
- Сумма: из строки в целые вей
- Адрес: регистр букв — это контрольная сумма
- Форма целиком
- Доступность: три атрибута
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Куда углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
Форма вклада — четыре элемента: поле суммы, поле адреса, кнопка и подпись об ошибке. Выглядит как самое простое место в проекте.
На деле здесь сходятся все способы потерять деньги: запятая вместо точки, лишний ноль в сумме, опечатка в адресе, отправка без проверки, двойное нажатие. Транзакцию нельзя отменить — значит, всё, что можно поймать до отправки, ловят до отправки.
Эта глава — про то, что на самом деле оказывается в полях, что проверяет браузер бесплатно и что придётся проверить самим.
Состояние формы — один объект
Полей обычно несколько, и заводить под каждое своё состояние неудобно: обработчик приходится писать для каждого.
const [form, setForm] = useState({ amount: "", to: "" });
const change = (e) =>
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
<input name="amount" value={form.amount} onChange={change} />
<input name="to" value={form.to} onChange={change} />
Поле «число» отдаёт не число
Кажется, что поле с типом «число» избавляет от разбора. Прогон показывает, что на самом деле в нём оказывается:
ввели value valueAsNumber
"2.5" "2.5" 2.5
"2,5" "" NaN
"1e5" "1e5" 100000
"--" "" NaN
"" "" NaN
"abc" "" NaN
"-3" "-3" -3
Три вывода, каждый из которых стоит денег.
Значение поля — всегда строка. Даже у поля с типом «число». Рядом есть числовой вид значения, но он даёт «не число» на всём, что не разобралось.
Запятая не попадает в значение вообще. Пользователь печатает «2,5», а в поле остаётся пусто — и с его точки зрения поле сломано. Это самая частая жалоба на форму суммы у русскоязычного пользователя.
Запись вроде 1e5 проходит. Браузер считает её числом 100 000. Дальше эту строку получит библиотека сумм — и вот тут начинается интересное.
Что браузер проверяет бесплатно
── встроенная проверка ──
пустое обязательное поле: valid=false, сообщение: "Заполните это поле."
число меньше min=0 : valid=false, сообщение: "Значение должно быть больше или равно 0."
адрес не по шаблону : valid=false, сообщение: "Введите данные в указанном формате."
правильный адрес : valid=true
форма целиком : valid=true
Сообщения приходят на языке браузера и бесплатно. Проверок довольно много:
valueMissing, typeMismatch, patternMismatch, tooLong, tooShort,
rangeUnderflow, rangeOverflow, stepMismatch, badInput, customError
Встроенную проверку удобно использовать как первый рубеж, а свои сообщения показывать рядом. Полностью отдавать ей проверку не стоит: она ничего не знает ни про контрольную сумму адреса, ни про то, хватает ли денег на балансе.
Когда показывать ошибку
Правило, которое делает форму приятной, ровно одно: не ругаться раньше, чем человек закончил.
- Во время набора — только убирать уже показанную ошибку, когда её причина исчезла.
- При уходе из поля — показывать ошибку этого поля.
- При отправке — показывать все ошибки сразу и ставить курсор в первое неверное поле.
Форма, которая краснеет на первой же введённой букве, воспринимается как враждебная. Форма, которая молчит до нажатия кнопки, заставляет искать ошибку глазами.
const [touched, setTouched] = useState({});
<input
name="to"
value={form.to}
onChange={change}
onBlur={() => setTouched((p) => ({ ...p, to: true }))}
aria-invalid={touched.to && !!errors.to}
aria-describedby="to-error"
/>
{touched.to && errors.to && <p id="to-error">{errors.to}</p>}
Сумма: из строки в целые вей
Дробных чисел в сети нет — есть целые вей. Между полем и транзакцией всегда стоит перевод, и он умеет отказываться:
── превращение строки из поля в сумму ──
"2.5" 2500000000000000000 wei
"2,5" ОТКАЗ: invalid FixedNumber string value
"" ОТКАЗ: invalid FixedNumber string value
"0.000000000000000001" 1 wei
"1e5" ОТКАЗ: invalid FixedNumber string value
"0.0000000000000000001" ОТКАЗ: too many decimals for format
"-1" -1000000000000000000 wei
Запятая и 1e5 отвергнуты. Обратите внимание на пару: 1e5 браузер принял как число, а перевод отверг. Значит, полагаться на то, что «поле числовое, значит там число», нельзя.
Девятнадцать знаков после точки — отказ. У эфира ровно восемнадцать; девятнадцатый некуда девать. Сообщение об этом понятное, и его стоит показать пользователю своими словами.
Минус прошёл. Библиотека честно перевела «минус один эфир» в отрицательное число вей. Отказ случится позже — уже при отправке транзакции, и выглядеть будет непонятно. Знак проверяют сами.
function parseAmount(text) {
const clean = text.trim().replace(",", ".");
if (clean === "") return { ok: false, error: "Введите сумму" };
let wei;
try { wei = parseEther(clean); }
catch { return { ok: false, error: "Не похоже на сумму: ждём, например, 2.5" }; }
if (wei <= 0n) return { ok: false, error: "Сумма должна быть больше нуля" };
return { ok: true, wei };
}
Адрес: регистр букв — это контрольная сумма
── проверка адреса ──
0xf39Fd6e51a…b92266 isAddress=true ок
0xf39fd6e51a…b92266 isAddress=true ок (всё строчными)
0xF39FD6E51A…B92266 isAddress=true ок (всё прописными)
0xf39Fd6e51a…b92267 isAddress=false ОТКАЗ: bad address checksum
0xf39Fd6e51a…Fb9226 isAddress=false ОТКАЗ: invalid address
f39Fd6e51aad…b92266 isAddress=true ок (без 0x)
Адрес — это сорок шестнадцатеричных цифр, и регистр букв в них не случаен. Он вычисляется из самого адреса и служит контрольной суммой: измените одну цифру — и регистр перестанет сходиться.
Из прогона следуют три практических вывода.
Опечатка ловится. Один изменённый символ в адресе смешанного регистра — и проверка отказывает. Это бесплатная защита от самой дорогой ошибки в интерфейсе кошелька.
Адрес целиком строчными проходит всегда. Проверять в нём нечего: регистр не несёт информации. Если пользователь вставил такой адрес, опечатку поймать нечем — остаётся показать ему адрес в правильном регистре и попросить сверить.
Приведение к правильному виду — часть проверки. Функция приведения возвращает адрес в смешанном регистре; его и показывают на экране и кладут в транзакцию.
Интерфейс кошелька обязан показывать адрес получателя крупно и целиком перед подписью. Сокращение вида «0xf39F…2266» удобно в списках и опасно в подтверждении: подделать начало и конец адреса — известный приём.
Форма целиком
function DepositForm({ onSubmit, busy }) {
const [form, setForm] = useState({ amount: "", to: "" });
const [touched, setTouched] = useState({});
const amount = parseAmount(form.amount);
const to = parseAddress(form.to);
const canSend = amount.ok && to.ok && !busy;
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(amount.wei, to.address); }}>
…
<button type="submit" disabled={!canSend}>
{busy ? "Отправляем…" : "Отправить"}
</button>
</form>
);
}
Три вещи, которые здесь сделаны намеренно.
Разбор — при каждом пересчёте, а не в обработчике. Он дешёвый, и благодаря этому состояние кнопки всегда соответствует содержимому полей.
Кнопка выключена, пока форма неверна или идёт отправка. Второе не менее важно: транзакция идёт секунды, и без блокировки пользователь отправит три.
Отмена поведения по умолчанию — первой строкой. Иначе страница перезагрузится.
Доступность: три атрибута
Форма — то место, где доступность стоит три строки и заметно влияет на оценку.
- Подпись, связанная с полем. Нажатие на подпись ставит курсор в поле, а программа чтения называет поле по имени.
- Признак ошибки на поле. Программа чтения сообщает «неверное значение», а не молчит.
- Связь поля с текстом ошибки. Тогда сообщение читается сразу после названия поля, а не теряется где-то на странице.
Плюс одно правило без атрибутов: сообщение должно говорить, что делать. «Неверный формат» — плохо. «Ждём адрес из 40 знаков, начинающийся с 0x» — хорошо.
Типичные ошибки
Запятую не принимают. Пользователь печатает — поле пустое.
Верят типу поля. «Числовое поле, значит там число» — а там 1e5 или пустая строка.
Не проверяют знак суммы. Отказ приходит позже и выглядит непонятно.
Проверяют адрес только по длине. Контрольная сумма ловит опечатку, длина — нет.
Ругаются на первой букве. Форма выглядит враждебной.
Кнопка не выключается на время отправки. Три нажатия — три транзакции.
Сообщение без подсказки. «Неверный формат» не говорит, что делать.
Сокращённый адрес в подтверждении. Начало и конец адреса подделывают намеренно.
Слова главы
- Состояние формы одним объектом — один обработчик на все поля, ключ вычисляется из имени.
- Значение поля — строка — даже у поля с типом «число».
- Встроенная проверка — обязательность, границы, шаблон; сообщения на языке браузера.
- Признак «поля касались» — чтобы не ругаться раньше времени.
- Перевод суммы — из строки в целые вей; отказывается на запятой, пустоте и лишних знаках.
- Контрольная сумма адреса — регистр букв, вычисленный из самого адреса.
- Приведение адреса — к смешанному регистру; его и показывают, и отправляют.
- Блокировка на время отправки — защита от двойного нажатия.
Тренажёр: набрать вслепую
Цель: скорость ≥ 95 зн/мин, точность ≥ 88%
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
На чемпионате
Запятая, знак, контрольная сумма — три строки, которые видно сразу. Проверяющий наберёт «2,5» и вставит адрес с опечаткой; это первое, что делают.
Кнопка с понятным состоянием. «Отправить» → «Отправляем…» → снова «Отправить». Ничего не стоит и выглядит законченно.
Сообщения на русском и по делу. Не «invalid FixedNumber string value», а «Не похоже на сумму: ждём, например, 2.5».
Адрес получателя перед подписью — целиком. Это и требование безопасности, и признак того, что вы понимаете, зачем интерфейс нужен.
И последнее: форму делают до красоты, но после того, как работает отправка. Идеально проверенная форма, которая никуда не отправляет, стоит меньше, чем грубая, но рабочая.
Куда углубиться
- Библиотеки форм — берут на себя состояние, касания и сообщения.
- Описание правил отдельно от формы — правила как данные, проверка одной функцией.
- Своё сообщение во встроенной проверке — как подменить текст браузера.
- Поле суммы с шагом от остатка — кнопка «всё» и почему она никогда не берёт весь баланс.
- Адресная книга — сохранённые получатели вместо ручного ввода; лучшая защита от опечаток.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- MDN: проверка форм — встроенные правила и свои сообщения.
- MDN: ValidityState — все признаки проверки.
- React: формы — поле, привязанное к состоянию, и его особенности.
- ethers: utils — перевод сумм и работа с адресами.
- EIP-55: контрольная сумма адреса — откуда берётся регистр букв.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.