Форма добавления и валидация

проект≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

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

Разделы темы:·

Форма — не просто рамка вокруг полей. Это объект, который уже знает про все свои элементы: form.elements отдаёт их и по номеру, и по атрибуту name. Бегать за каждым полем через querySelector не нужно — форма собрала их за нас. Как устроен этот доступ и почему value остаётся строкой даже у type="number", разбирает статья «Элементы формы и их значения».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Последняя строка не опечатка: кнопка — тоже элемент формы, поэтому полей «видимых глазом» два, а элементов три. Когда полей становится больше двух, брать их поимённо надоедает. FormData обходит форму сам и собирает пары «имя — значение»; поля без name и кнопки без имени в сборку не попадают. Дальше Object.fromEntries превращает это в обычный объект — ровно тот, который потом уедет в список задач. Перебор, повторяющиеся ключи и отправку тела через fetch разбирает статья «FormData и отправка на сервер».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

По умолчанию отправка формы — это переход: браузер собирает поля, уходит по адресу из action и рисует страницу заново. Для интерактивной страницы это катастрофа — всё накопленное состояние обнуляется. Поэтому слушаем событие submit и первым делом вызываем event.preventDefault().

Слушать нужно именно форму, а не кнопку. Событие submit возникает и от клика по кнопке, и от Enter в текстовом поле, и от form.requestSubmit() — обработчик, повешенный на кнопку, знает только про первый случай. А чем requestSubmit() отличается от старого form.submit(), который отправляет молча, мимо проверки и мимо обработчиков, разбирает статья «Отправка формы и событие submit».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

preventDefault() — первая строка обработчика, а не последняя. Если сдвинуть её вниз, за проверку с ранним return, форма уйдёт на перезагрузку ровно в том случае, когда данные не в порядке: пользователь ошибся и тут же потерял всё, что набрал.

Браузер взял бы те же самые пары «имя — значение», склеил бы из них строку запроса и отправил её по адресу из action методом из method. Это не магия, а тот же FormData, только собранный самим браузером — в чём легко убедиться.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Часть работы браузер делает бесплатно. Атрибуты required, minlength, pattern и подходящий type — это готовая проверка, которую не надо писать. form.requestSubmit() и обычный клик по кнопке её запускают: если форма не проходит, событие submit не рождается вовсе, а браузер сам показывает подсказку у поля. Набор флагов validity, своё сообщение через setCustomValidity и отказ от браузерных подсказок через novalidate разбирает статья «Встроенная валидация и своя».

АтрибутЧто отсекает браузерЧто остаётся нам
requiredполностью пустое полестроку из одних пробелов
minlength / maxlengthслишком короткий и слишком длинный вводпонятный текст вместо системной подсказки
type="email"строку без собачки и доменапроверку, что адрес вообще существует
patternнесовпадение с регулярным выражениемобъяснение, какой формат мы ждём
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Встроенная проверка грубая, и это нормально: она про формат, а не про смысл. Для required три пробела — такой же текст, как любой другой, так что поле считается заполненным. Смысловая проверка всегда наша, и начинается она с trim().

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Отказ без объяснения — худшее, что форма может сделать. У сообщения должно быть постоянное место в разметке: отдельный узел рядом с полем, который в спокойном состоянии просто пуст. Атрибут role="alert" заставит скринридер прочитать появившийся текст, а aria-invalid на поле скажет, к чему этот текст относится.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Второй обработчик здесь важнее первого. Ошибку показывают по submit, а убирают по input — на первом же исправленном символе. Иначе красный текст висит под полем, пока пользователь не отправит форму снова, и ругается на то, что он уже поправил.

Задача добавлена — форма должна быть готова к следующей. form.reset() чистит все поля разом, включая те, до которых мы не дотрагивались, а focus() возвращает курсор в первое поле: пользователь печатает дальше, не касаясь мыши. И то и другое — только на успешной ветке. При ошибке введённое остаётся на месте.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

reset() возвращает поля не к пустой строке, а к значению по умолчанию — тому, что записано в атрибуте value в разметке. В примере выше поля были пустыми изначально, поэтому разницы не видно. А вот если подставить в разметку значение, оно вернётся.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

  • Форма знает про свои поля сама: form.elements — доступ по имени, FormData плюс Object.fromEntries — вся пачка значений разом.
  • Событие submit слушают на форме, а не на кнопке: так ловятся и Enter, и requestSubmit(). Первая строка обработчика — event.preventDefault().
  • Атрибуты required, minlength, pattern отсекают грубые ошибки бесплатно, но для них строка из пробелов — обычный текст. Смысловая проверка начинается с trim().
  • Сообщение об ошибке живёт в постоянном узле рядом с полем, дополняется атрибутом aria-invalid и снимается на первый же input.
  • После успешного добавления — form.reset() и focus() в первое поле; помните, что reset() возвращает поля к атрибутам value, а не к пустоте.

1/3В форме есть <input name="title" required> с пустым значением. Обработчик submit вызывает preventDefault() и увеличивает счётчик. Каким станет счётчик после form.requestSubmit()?

2/3Поле с атрибутом required содержит три пробела. Что вернёт field.checkValidity()?

3/3У поля в разметке стоит value="черновик". Пользователь ввёл своё название, обработчик вызвал form.reset(). Что теперь в input.value?

Добавление задачи с проверкой

Соберите обработчик целиком. По submit: отменить отправку, взять название и обрезать пробелы. Если осталась пустая строка — написать в #error текст «Название не может быть пустым», пометить поле через aria-invalid и на этом остановиться. Если название есть — добавить <li> в список, очистить форму и вернуть фокус в поле. И отдельно: любой ввод в поле должен убирать сообщение об ошибке.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Две детали, которые легко потерять. Первая: form.reset() вызывается только на успешной ветке — при ошибке набранное должно остаться. Вторая: снятие ошибки вынесено в обработчик input, а не продублировано в успешной ветке submit; очистка поля сама породит событие? Нет, программная запись значения событий не шлёт, но reset() здесь и не оставляет ошибочного текста — до него мы доходим только тогда, когда ошибки не было.

Счётчик оставшихся символов

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Подсказку держит в актуальном состоянии одна функция, подписанная на input. Её же зовут сразу после подписки, чтобы число появилось до первого ввода.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверку добавляем в уже существующий обработчик submit из первой задачи — сразу после preventDefault() и обрезки:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

В настоящем проекте половину этого сделал бы атрибут maxlength="40": браузер просто не дал бы ввести лишнее. Ручной счётчик нужен там, где лимит считается не по символам — например, по словам, — или где пользователю важно видеть остаток заранее.