Перейти к основному содержимому
Блокчейн46ГЛАВА 46Формы: ввод, проверка и суммы2.5ETH0xf39F…2267bad address checksumОтправить2.5 → 2500000000000000000

Формы: ввод, проверка и суммы

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Форма вклада — четыре элемента: поле суммы, поле адреса, кнопка и подпись об ошибке. Выглядит как самое простое место в проекте.

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

Эта глава — про то, что на самом деле оказывается в полях, что проверяет браузер бесплатно и что придётся проверить самим.

Состояние формы — один объект​

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

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
ПОЛЕ type="number": ЧТО В НЁМ ОКАЗЫВАЕТСЯввелиvalue (строка)valueAsNumberчто дальше сделает parseEther2.5"2.5"2.5ок: 2500000000000000000 wei2,5""NaNполе уже пустое1e5"1e5"100000ОТКАЗ: invalid FixedNumber stringabc""NaNполе уже пустое-3"-3"-3ок: отрицательная сумма!запятая и буквы не попадают в значение вовсе: пользователь печатает, а поле остаётся пустымпоказательная строка — 1e5: браузер её принял, а библиотека сумм отвергла
Запятая и буквы не попадают в значение вовсе; зато запись вроде 1e5 проходит насквозь

Три вывода, каждый из которых стоит денег.

Значение поля — всегда строка. Даже у поля с типом «число». Рядом есть числовой вид значения, но он даёт «не число» на всём, что не разобралось.

Запятая не попадает в значение вообще. Пользователь печатает «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
Нажми на часть выражения
Совет

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

Отвечено вопросов: 0 из 2
Пользователь печатает «2,5» в поле типа «число». Что окажется в значении?
Что даёт поле с типом «число» в свойстве значения?
Отвечено верно: 0 из 2

Когда показывать ошибку​

Правило, которое делает форму приятной, ровно одно: не ругаться раньше, чем человек закончил.

  • Во время набора — только убирать уже показанную ошибку, когда её причина исчезла.
  • При уходе из поля — показывать ошибку этого поля.
  • При отправке — показывать все ошибки сразу и ставить курсор в первое неверное поле.

Форма, которая краснеет на первой же введённой букве, воспринимается как враждебная. Форма, которая молчит до нажатия кнопки, заставляет искать ошибку глазами.

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
СТРОКА ИЗ ПОЛЯ → ЦЕЛОЕ ЧИСЛО ВЕЙ"2.5"2500000000000000000 wei"0.000000000000000001"1 wei — минимальная доля"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)
РЕГИСТР БУКВ В АДРЕСЕ — ЭТО КОНТРОЛЬНАЯ СУММА0xf39Fd6e51a…cffFb92266смешанный регистр, всё верно0xf39fd6e51a…cfffb92266всё строчными — проверять нечем, принят0xf39Fd6e51a…cffFb92267одна цифра измененаbad address checksum0xf39Fd6e51a…ffFb9226на символ корочеinvalid addressадрес в смешанном регистре сам себя проверяет: опечатка в одном символе поймана до отправкиадрес целиком строчными проходит всегда — контрольной суммы в нём просто нетпоэтому адрес, введённый руками, прогоняют через проверку, а не просто смотрят на длину
Опечатка в одном символе поймана контрольной суммой; адрес целиком строчными проверить нечем

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

Из прогона следуют три практических вывода.

Опечатка ловится. Один изменённый символ в адресе смешанного регистра — и проверка отказывает. Это бесплатная защита от самой дорогой ошибки в интерфейсе кошелька.

Адрес целиком строчными проходит всегда. Проверять в нём нечего: регистр не несёт информации. Если пользователь вставил такой адрес, опечатку поймать нечем — остаётся показать ему адрес в правильном регистре и попросить сверить.

Приведение к правильному виду — часть проверки. Функция приведения возвращает адрес в смешанном регистре; его и показывают на экране и кладут в транзакцию.

Важно

Интерфейс кошелька обязан показывать адрес получателя крупно и целиком перед подписью. Сокращение вида «0xf39F…2266» удобно в списках и опасно в подтверждении: подделать начало и конец адреса — известный приём.

Отвечено вопросов: 0 из 2
Почему в адресе важен регистр букв?
parseEther("-1") отработал без отказа. Что из этого следует?
Отвечено верно: 0 из 2

Форма целиком​

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 или пустая строка.

Не проверяют знак суммы. Отказ приходит позже и выглядит непонятно.

Проверяют адрес только по длине. Контрольная сумма ловит опечатку, длина — нет.

Ругаются на первой букве. Форма выглядит враждебной.

Кнопка не выключается на время отправки. Три нажатия — три транзакции.

Сообщение без подсказки. «Неверный формат» не говорит, что делать.

Сокращённый адрес в подтверждении. Начало и конец адреса подделывают намеренно.

Отвечено вопросов: 0 из 2
Зачем выключать кнопку отправки на время транзакции?
Почему адрес получателя перед подписью показывают целиком, а не сокращённо?
Отвечено верно: 0 из 2

Слова главы​

  • Состояние формы одним объектом — один обработчик на все поля, ключ вычисляется из имени.
  • Значение поля — строка — даже у поля с типом «число».
  • Встроенная проверка — обязательность, границы, шаблон; сообщения на языке браузера.
  • Признак «поля касались» — чтобы не ругаться раньше времени.
  • Перевод суммы — из строки в целые вей; отказывается на запятой, пустоте и лишних знаках.
  • Контрольная сумма адреса — регистр букв, вычисленный из самого адреса.
  • Приведение адреса — к смешанному регистру; его и показывают, и отправляют.
  • Блокировка на время отправки — защита от двойного нажатия.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 95 зн/мин, точность ≥ 88%

setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));

На чемпионате​

Запятая, знак, контрольная сумма — три строки, которые видно сразу. Проверяющий наберёт «2,5» и вставит адрес с опечаткой; это первое, что делают.

Кнопка с понятным состоянием. «Отправить» → «Отправляем…» → снова «Отправить». Ничего не стоит и выглядит законченно.

Сообщения на русском и по делу. Не «invalid FixedNumber string value», а «Не похоже на сумму: ждём, например, 2.5».

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

И последнее: форму делают до красоты, но после того, как работает отправка. Идеально проверенная форма, которая никуда не отправляет, стоит меньше, чем грубая, но рабочая.

Куда углубиться​

  • Библиотеки форм — берут на себя состояние, касания и сообщения.
  • Описание правил отдельно от формы — правила как данные, проверка одной функцией.
  • Своё сообщение во встроенной проверке — как подменить текст браузера.
  • Поле суммы с шагом от остатка — кнопка «всё» и почему она никогда не берёт весь баланс.
  • Адресная книга — сохранённые получатели вместо ручного ввода; лучшая защита от опечаток.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоФормы в React: управление вводом и простая валидация без сторонних библиотекАлександр Ламков — Friendly Frontend
ВидеоJavaScript валидация форм — validity свойство у полей формы. Классовый компонент валидации формАлександр Ламков — Friendly Frontend
Видео14. Формы в HTML. Атрибуты required, disabled, readonly, placeholder, inputmode, type и autocompleteАлександр Ламков — Friendly Frontend
ВидеоВалидация форм на React JS. Простая валидация. Урок 1.Ulbi TV
Видео013. Нативная валидация как фреймворк — Павел ЛовцевичYandex for Frontend

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.