MakeFirstStep· Frontend + ИИ

Урок 10 · DOM и интерфейс

Формы и валидация

Регистрация, вход, заказ плова с доставкой, запись на курс — почти всё, что пользователь «даёт» сайту, он вводит через форму. А пользователи ошибаются: пропускают поля, пишут телефон без кода страны, ставят пробел в конец email. Сегодня вы научитесь читать данные из формы и вежливо объяснять человеку, что нужно исправить, — ещё до отправки.

  • Читать value и checked у разных полей: текст, число, радиокнопки, чекбоксы, список
  • Перехватывать отправку формы событием submit и собирать данные через FormData
  • Пользоваться встроенной валидацией браузера и понимать, чего ей не хватает
  • Писать простые регулярные выражения и применять их в проверках
  • Писать свои функции-валидаторы и выводить понятные тексты ошибок прямо под полями
  • Делать «живую» валидацию: проверять поле, пока человек вводит данные
Как работать с этой страницей

Код на тёмном фоне с кнопкой ▶ Запустить — живой: меняйте его и смотрите результат. В консольных блоках форма создаётся прямо из строки HTML — так мы можем проверить код без ручного ввода. Окна с вкладками index.html / style.css / script.js — мини-страницы: сверху код, снизу превью, в котором можно по-настоящему кликать и печатать. Запуск — Ctrl + Enter. Подсказки к задачам спрятаны: сначала попробуйте сами 15 минут.

01 Форма и её поля: value и checked

Форма в HTML — это тег <form>, внутри которого лежат поля ввода: <input>, <select>, <textarea> и кнопка отправки. Вёрстку вы уже знаете — сегодня нас интересует, как достать из этих полей данные с помощью JavaScript.

Аналогия из жизни

Анкета в банке или бланк заказа в чайхоне: у каждой строки есть название («Имя», «Телефон») и то, что вы вписали. В форме название — это атрибут name, а то, что вписано, — свойство value. Оператор берёт заполненный бланк и читает строку за строкой — ровно так же работает ваш скрипт.

Как найти поле

Каждое поле можно найти через querySelector, но у формы есть удобный путь — коллекция form.elements. В ней поля доступны по атрибуту name:

Читаем поля формы · Ctrl + Enter
<form id="reg">
  <label>Имя <input name="fullName" value="Мадина"></label>
  <label>Возраст <input name="age" type="number" value="19"></label>
  <label>Город
    <select name="city">
      <option value="dushanbe">Душанбе</option>
      <option value="khujand">Худжанд</option>
    </select>
  </label>
  <label><input type="checkbox" name="agree"> Согласен с правилами</label>
  <button type="button" id="show">Показать значения</button>
</form>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Запустите, поменяйте что-нибудь в превью (введите другое имя, поставьте галочку) и нажмите «Показать значения». Вот что и откуда мы читаем:

ПолеЧто читаемЧто получим
<input type="text">, email, password, tel.valueСтрока — ровно то, что в поле (с пробелами)
<input type="number">.valueТоже строка! "19", не число. Пустое поле — ""
<textarea>.valueСтрока (с переводами строк \n)
<select>.valueАтрибут value выбранного <option>, а не его видимый текст
<input type="checkbox">.checkedtrue или false
Группа <input type="radio"> с одним nameform.elements.имя.valuevalue выбранной кнопки (или "", если ничего не выбрано)

Главная ловушка: value — всегда строка

Даже у type="number" значение приходит строкой. Это та самая история с приведением типов из урока 2:

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <input name="plov" type="number" value="40">
  <input name="tea" type="number" value="10">`;

const plov = document.querySelector("[name=plov]").value;
const tea = document.querySelector("[name=tea]").value;

console.log(plov + tea);                   // "4010" — склеились как строки!
console.log(Number(plov) + Number(tea));   // 50 — сначала превращаем в числа
console.log(document.querySelector("[name=plov]").valueAsNumber); // 40 — число сразу
Частые ошибки

1) Читать у чекбокса .value вместо .checked — вы всегда получите строку "on", даже если галочки нет. 2) Забыть .value и вывести сам элемент: console.log(form.elements.fullName) покажет тег, а не текст. 3) Сложить два числовых поля через + — получится склейка строк.

Атрибуты name, label и type

  • name — «имя поля». Через него мы находим поле в form.elements, и именно по нему данные попадают в FormData (раздел 3). Без name поле «невидимо» для формы.
  • <label> — подпись. Оберните в неё поле или свяжите через for и id: тогда клик по тексту ставит курсор в поле, а экранные читалки понимают, что за поле перед ними.
  • type — подсказывает браузеру, какую клавиатуру показать на телефоне (tel — цифровую, email — с «@») и какие проверки включить.

Читать подробнее: Свойства и методы формы · MDN: элемент input

Практика: закрепляем

В консольных задачах формы создаются из строки HTML. Ваша работа — прочитать нужные значения.

ЗАДАЧА 1.1

Анкета студента

лёгкая

В форме уже есть значения. Выведите их тремя строками в формате, как в примере: имя, город и курс.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="fullName" value="Мадина Каримова">
    <input name="city" value="Душанбе">
    <input name="course" value="Frontend + AI">
  </form>`;

const form = document.querySelector("form");

// выведите три строки через console.log и form.elements
Ожидаемый результат
Имя: Мадина Каримова
Город: Душанбе
Курс: Frontend + AI
Подсказка 1

Поле находим так: form.elements.fullName — по значению атрибута name.

Подсказка 2

Значение — это свойство .value. Склейте строку: console.log("Имя: " + form.elements.fullName.value);

ЗАДАЧА 1.2

Счёт в чайхоне

лёгкая

Кассир вводит стоимость плова и чая в числовые поля. Код должен вывести итог и итог со скидкой 10%, но сейчас выводит ерунду вроде 4010. Исправьте чтение значений, не трогая строки с console.log.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="plov" type="number" value="40">
    <input name="tea" type="number" value="10">
  </form>`;

const form = document.querySelector("form");
const plov = form.elements.plov.value;
const tea = form.elements.tea.value;

console.log("Итого:", plov + tea);
console.log("Со скидкой 10%:", (plov + tea) * 0.9);
Ожидаемый результат
Итого: 50
Со скидкой 10%: 45
Подсказка 1

Оператор + со строками склеивает их. Нужно, чтобы plov и tea были числами.

Подсказка 2

Превратите значения в числа сразу при чтении: const plov = Number(form.elements.plov.value); — и то же для tea.

ЗАДАЧА 1.3

Радиокнопки, чекбоксы и список

средняя

Форма записи на курс содержит список, группу радиокнопок и два чекбокса. Выведите четыре строки. Для чекбоксов выводите именно true / false.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <select name="course">
      <option value="html">HTML и CSS</option>
      <option value="js" selected>JavaScript</option>
    </select>
    <input type="radio" name="format" value="offline">
    <input type="radio" name="format" value="online" checked>
    <input type="checkbox" name="agree" checked>
    <input type="checkbox" name="news">
  </form>`;

const form = document.querySelector("form");

// Курс: ...  Формат: ...  Согласие: ...  Рассылка: ...
Ожидаемый результат
Курс: js
Формат: online
Согласие: true
Рассылка: false
Подсказка 1

Для списка и для группы радиокнопок подойдёт form.elements.имя.value. Для чекбоксов — .checked.

Подсказка 2

Радиокнопки с одинаковым name образуют группу: form.elements.format.value вернёт значение выбранной ("online").

02 Отправка формы: событие submit

Когда человек нажимает кнопку «Отправить» или жмёт Enter в текстовом поле, форма генерирует событие submit. Если ничего не делать, браузер отправит данные на сервер и перезагрузит страницу — все введённые данные и вся ваша JS-логика пропадут. Поэтому первая строка любого обработчика отправки — event.preventDefault() (вы знаете это из урока 9).

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form id="order">
    <input name="dish" value="Оши палав">
    <input name="qty" type="number" value="2">
    <button>Заказать</button>
  </form>`;

const form = document.querySelector("#order");

form.addEventListener("submit", (event) => {
  event.preventDefault();                       // не даём странице перезагрузиться
  const dish = form.elements.dish.value;        // читаем значения ВНУТРИ обработчика
  const qty = Number(form.elements.qty.value);
  console.log(`Заказ принят: ${dish} × ${qty}`);
});

form.dispatchEvent(new Event("submit", { cancelable: true }));   // имитируем нажатие кнопки «Заказать»
1 · Вводчеловек заполняет поля
→
2 · Отправкаклик по кнопке или Enter
→
3 · submitсобытие на <form>
→
4 · Ваш кодpreventDefault(), чтение полей, проверка

Слушаем submit, а не click

Плохо: click на кнопке

Если человек отправит форму клавишей Enter, кнопку никто не нажимал — ваш код не сработает.

JavaScript
button.addEventListener("click", () => {
  // Enter в поле сюда не попадёт
});
Хорошо: submit на форме

Срабатывает при любом способе отправки: мышь, Enter, кнопка на телефоне.

JavaScript
form.addEventListener("submit", (event) => {
  event.preventDefault();
  // ...
});

Полезные детали

event.target
В событии submit это сама форма. Значит, поля можно взять как event.target.elements.email — обработчик не зависит от переменной снаружи.
form.reset()
Возвращает все поля к начальным значениям (тем, что записаны в атрибутах value / checked, или пустым). Вызываем после успешной отправки.
form.dispatchEvent(new Event("submit", { cancelable: true }))
Вручную «стреляет» событием submit. Так мы имитируем нажатие «Отправить» в примерах и задачах: учебная песочница блокирует настоящую отправку форм, поэтому обычный form.requestSubmit() (он нажимает кнопку сам) здесь не сработает. На обычном сайте вы кликаете по кнопке или вызываете именно requestSubmit().
<button type="button">
Кнопка внутри формы по умолчанию отправляет её. Если кнопка нужна для другого (например, «показать пароль»), пишите type="button".
novalidate
Атрибут формы: отключает встроенную проверку браузера, чтобы работала ваша собственная (раздел 4).
Типичный баг: читаем значение слишком рано

Значение поля меняется, пока человек печатает. Поэтому читать его надо в момент события, внутри обработчика. Если прочитать один раз в начале скрипта, вы получите то, что было в поле при загрузке страницы (чаще всего — пустую строку).

Баг: пустой email · Ctrl + Enter
document.body.innerHTML = `<form><input name="email"></form>`;
const form = document.querySelector("form");

const email = form.elements.email.value;   // ✖ прочитали сразу — там ещё пусто

form.addEventListener("submit", (event) => {
  event.preventDefault();
  console.log("email:", "[" + email + "]");
});

form.elements.email.value = "madina@mail.tj";  // «пользователь» вписал адрес
form.dispatchEvent(new Event("submit", { cancelable: true }));                          // → email: []
Аналогия из жизни

Официант в чайхоне записывает заказ не тогда, когда вы только сели за стол, а когда вы сказали «можно заказывать». Значения в форме читаем так же — в момент отправки, а не заранее.

Читать подробнее: Отправка формы: событие и метод submit · События change, input, cut, copy, paste

Практика: закрепляем

Во всех задачах скрипт сам «заполняет» поля и вызывает событие submit — вам нужно написать обработчик.

ЗАДАЧА 2.1

Заявка принята

лёгкая

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

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="fullName" value="Рустам">
    <input name="phone" value="+992927771234">
  </form>`;

const form = document.querySelector("form");

// добавьте обработчик submit здесь


form.dispatchEvent(new Event("submit", { cancelable: true }));
Ожидаемый результат
Заявка принята: Рустам, +992927771234
Подсказка 1

form.addEventListener("submit", (event) => { ... }) — а первой строкой внутри event.preventDefault().

Подсказка 2

Значения читайте внутри обработчика: form.elements.fullName.value и form.elements.phone.value.

ЗАДАЧА 2.2

Найди баг: город пропал

лёгкая

Пользователь вписал «Худжанд», но обработчик печатает пустой город. Найдите причину и исправьте. Две последние строки скрипта (заполнение и отправка) менять нельзя.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `<form><input name="city"></form>`;
const form = document.querySelector("form");

const city = form.elements.city.value;

form.addEventListener("submit", (event) => {
  event.preventDefault();
  console.log("Город:", city);
});

form.elements.city.value = "Худжанд";   // не менять
form.dispatchEvent(new Event("submit", { cancelable: true }));                    // не менять
Ожидаемый результат
Город: Худжанд
Подсказка 1

Когда выполняется строка const city = ...? До того, как значение поля поменяли.

Подсказка 2

Перенесите чтение значения внутрь обработчика submit.

ЗАДАЧА 2.3

Форма входа

средняя

Напишите обработчик формы входа. Логин нужно очистить от пробелов по краям (trim()). Если логин или пароль пустые — выведите Ошибка: заполните все поля. Иначе выведите Вход выполнен: логин, сбросьте форму и сообщите, пустое ли после этого поле логина.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="login">
    <input name="password" type="password">
  </form>`;

const form = document.querySelector("form");

// обработчик submit


function submitWith(login, password) {
  form.elements.login.value = login;
  form.elements.password.value = password;
  form.dispatchEvent(new Event("submit", { cancelable: true }));
}

submitWith("", "12345");
submitWith("  madina  ", "");
submitWith("  madina  ", "qwerty1");
Ожидаемый результат
Ошибка: заполните все поля
Ошибка: заполните все поля
Вход выполнен: madina
Логин после сброса: пусто
Подсказка 1

Порядок: preventDefault → прочитать и trim() логин → прочитать пароль → проверка if (!login || !password) → return после сообщения об ошибке.

Подсказка 2

После успешного входа: form.reset(), затем console.log("Логин после сброса:", form.elements.login.value === "" ? "пусто" : "заполнено").

03 FormData: собираем все поля разом

Если в форме 10 полей, читать их по одному через .value — скучно и легко что-то забыть. Для этого в браузере есть встроенный объект FormData: вы отдаёте ему форму, а он сам обходит все поля с атрибутом name и собирает их «имя → значение».

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="fullName" value="Мадина">
    <select name="city">
      <option value="dushanbe" selected>Душанбе</option>
      <option value="khujand">Худжанд</option>
    </select>
    <input type="checkbox" name="skills" value="css" checked>
    <input type="checkbox" name="skills" value="js" checked>
    <input type="checkbox" name="skills" value="git">
    <input type="checkbox" name="agree" checked>
    <input type="checkbox" name="news">
  </form>`;

const form = document.querySelector("form");
const data = new FormData(form);

console.log(data.get("fullName"));    // Мадина — одно значение по имени
console.log(data.getAll("skills"));   // ["css", "js"] — все значения одного имени
console.log(data.has("news"));        // false — чекбокс без галочки в FormData не попадает
console.log(data.get("agree"));       // "on" — у чекбокса без атрибута value значение "on"

for (const [key, value] of data) {    // FormData можно перебрать как пары «ключ, значение»
  console.log(key, "=", value);
}

Из FormData легко получить привычный объект — он пригодится, чтобы сохранить данные или показать их пользователю (упакованный в JSON, как на уроке 7):

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="fullName" value="Мадина">
    <input name="age" type="number" value="19">
  </form>`;

const data = new FormData(document.querySelector("form"));
const obj = Object.fromEntries(data);   // { fullName: "Мадина", age: "19" }

console.log(JSON.stringify(obj));
console.log(typeof obj.age);   // string — все значения по-прежнему строки!
Аналогия из жизни

Это как почтовый курьер, который забирает из офиса все заполненные бланки одним пакетом. Но в пакет он берёт только бланки с подписью (name) и только те, которые заполнены. Поле без name — безымянный бланк, его курьер не возьмёт.

Что не попадает в FormData
  • поля без атрибута name;
  • поля с атрибутом disabled (для «только чтения» используйте readonly);
  • чекбоксы и радиокнопки без галочки — их ключа в данных просто нет. Если нужно false, берите checked напрямую: form.elements.news.checked.

А ещё Object.fromEntries оставляет только последнее значение для повторяющихся имён (skills), поэтому для групп чекбоксов используйте data.getAll("skills").

Позже, когда мы дойдём до работы с сервером, этот же объект FormData можно будет отправить на сервер как есть. А пока наша цель — собрать данные и убедиться, что они правильные.

Читать подробнее: FormData · MDN: FormData

Практика: закрепляем

Три задачи — от простого чтения до функции, которая превращает форму в готовый объект.

ЗАДАЧА 3.1

Три значения из FormData

лёгкая

Создайте FormData из формы и выведите имя, email и город — каждое в своей строке.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="fullName" value="Зарина Ахмедова">
    <input name="email" value="zarina@mail.tj">
    <select name="city">
      <option value="dushanbe" selected>Душанбе</option>
      <option value="khujand">Худжанд</option>
    </select>
  </form>`;

const form = document.querySelector("form");

// создайте FormData и выведите три значения через data.get(...)
Ожидаемый результат
Имя: Зарина Ахмедова
Email: zarina@mail.tj
Город: dushanbe
Подсказка 1

const data = new FormData(form); — форма передаётся в скобках.

Подсказка 2

data.get("email") вернёт значение поля с name="email".

ЗАДАЧА 3.2

Почини форму: пропавшие поля

средняя

Из трёх полей в FormData попало только одно. Исправьте HTML формы (не JS), чтобы в итоговом объекте было три поля: fullName, phone и agree. Телефон должен остаться нередактируемым для пользователя, но попадать в данные.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input value="Далер">
    <input name="phone" value="+992900000000" disabled>
    <input type="checkbox" name="agree" checked>
  </form>`;

const form = document.querySelector("form");
console.log(JSON.stringify(Object.fromEntries(new FormData(form))));
Ожидаемый результат
{"fullName":"Далер","phone":"+992900000000","agree":"on"}
Подсказка 1

Первому полю не хватает одного атрибута, который даёт ему «имя» в данных формы.

Подсказка 2

У второго поля замените disabled на readonly: оно по-прежнему нередактируемое, но уже участвует в отправке.

ЗАДАЧА 3.3

Функция readForm

сложная

Напишите функцию readForm(form), которая возвращает объект с такими полями в таком порядке: fullName (строка), age (число), skills (массив отмеченных навыков), agree и news (true/false).

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="fullName" value="Рустам">
    <input name="age" type="number" value="21">
    <input type="checkbox" name="skills" value="html" checked>
    <input type="checkbox" name="skills" value="css">
    <input type="checkbox" name="skills" value="js" checked>
    <input type="checkbox" name="agree" checked>
    <input type="checkbox" name="news">
  </form>`;

function readForm(form) {
  // верните объект { fullName, age, skills, agree, news }
}

console.log(JSON.stringify(readForm(document.querySelector("form"))));
Ожидаемый результат
{"fullName":"Рустам","age":21,"skills":["html","js"],"agree":true,"news":false}
Подсказка 1

Начните с const data = new FormData(form);. Строку и массив берите оттуда: data.get(...) и data.getAll(...).

Подсказка 2

Возраст — Number(data.get("age")). Чекбоксы в FormData могут отсутствовать, поэтому для них надёжнее form.elements.agree.checked.

04 Встроенная валидация браузера

Валидация — это проверка, что данные подходят: поле не пустое, email похож на email, возраст в разумных пределах. У браузера уже есть готовая проверка — её включают атрибуты HTML, а JavaScript может её опросить.

АтрибутЧто проверяетПример
requiredПоле обязательно<input required>
type="email", urlФормат адреса (есть @, есть домен)<input type="email">
min, maxГраницы числа (и даты)<input type="number" min="16" max="70">
minlength, maxlengthДлина текста<input minlength="2" maxlength="30">
patternТекст подходит под шаблон (регулярное выражение — раздел 5)<input pattern="[0-9]{4}">

Попробуйте прямо в превью: оставьте поле пустым или впишите «ali@» и нажмите «Отправить». Браузер сам покажет всплывающую подсказку и не пропустит форму дальше.

Встроенная валидация · Ctrl + Enter
<form id="f">
  <label>Имя <input name="fullName" required></label>
  <label>Email <input name="email" type="email" required></label>
  <label>Возраст <input name="age" type="number" min="16" max="70"></label>
  <button>Отправить</button>
</form>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Спросим у браузера из JavaScript

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

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="email" type="email" required value="madina@">
  </form>`;

const email = document.querySelector("[name=email]");

console.log(email.checkValidity());          // false — поле не проходит проверку
console.log(email.validity.valid);           // false — то же самое, но как свойство
console.log(email.validity.typeMismatch);    // true — «тип не тот»: это не email
console.log(email.validity.valueMissing);    // false — поле не пустое

email.value = "madina@mail.tj";
console.log(email.checkValidity());          // true — теперь всё хорошо
checkValidity()
Возвращает true / false. У формы проверяет все её поля сразу.
reportValidity()
То же, но ещё и показывает пользователю всплывающую подсказку браузера.
validity
Объект с флагами причин: valueMissing (пусто при required), typeMismatch (не email/url), patternMismatch (не подошёл pattern), rangeUnderflow / rangeOverflow (меньше min / больше max), tooShort / tooLong, customError.
validationMessage
Текст ошибки на языке браузера пользователя. У одного человека он русский, у другого английский — управлять им нельзя.
setCustomValidity("текст")
Вручную объявляет поле невалидным с вашим сообщением. Пустая строка "" снимает ошибку.
setCustomValidity: свои правила · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="password" type="password" value="qwerty123">
    <input name="confirm" type="password" value="qwerty321">
  </form>`;

const { password, confirm } = document.querySelector("form").elements;

confirm.setCustomValidity(password.value === confirm.value ? "" : "Пароли не совпадают");

console.log(confirm.checkValidity());        // false — пароли разные
console.log(confirm.validationMessage);      // Пароли не совпадают
Аналогия из жизни

Встроенная валидация — как охранник на входе в торговый центр: он смотрит на очевидное (есть ли билет, не пустой ли пропуск), но не знает правил вашего магазина. Он не скажет «Вам нужен именно таджикский номер с кодом +992» и не объяснит причину отказа на понятном вам языке и в нужном месте страницы. Для этого нужна собственная проверка.

Чего не хватает встроенной валидации

  • Тексты ошибок — на языке и в стиле браузера, их не оформить по дизайну сайта.
  • Один пузырёк за раз — человек видит только первую ошибку, а не все сразу.
  • Сложные правила — «пароли совпадают», «телефон только таджикский» требуют своего кода.
  • Живая проверка по ходу ввода — «красиво» без своего JavaScript не получится.
Нюансы

1) minlength и maxlength проверяются только когда значение вводит человек. Если записать input.value = "a" из JavaScript, флаг tooShort не сработает. 2) Все проверки браузера и ваш JS на странице легко обойти (достаточно открыть DevTools), поэтому на настоящем сайте данные проверяют ещё и на сервере. Проверка в браузере — это удобство для человека, а не защита.

Поэтому на практике делают так: ставят базовые атрибуты (required, type) ради подсказок мобильной клавиатуры, а саму проверку и тексты ошибок пишут в JavaScript и отключают браузерные пузырьки атрибутом novalidate на форме:

JavaScript
<form id="reg" novalidate>
  <input name="email" type="email" required>
</form>

Читать подробнее: MDN: валидация форм на стороне клиента · MDN: ValidityState

Практика: закрепляем

Опрашиваем браузер и учимся переключаться на собственную проверку.

ЗАДАЧА 4.1

Кто невалиден

лёгкая

В форме четыре поля. Пройдите по form.elements и выведите одной строкой имена полей, которые не проходят проверку браузера, через запятую.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="fullName" required value="">
    <input name="email" type="email" value="madina@">
    <input name="age" type="number" min="16" max="70" value="15">
    <input name="phone" pattern="\\+992[0-9]{9}" value="+992927771234">
  </form>`;

const form = document.querySelector("form");
const bad = [];

// пройдите по form.elements и наполните массив bad именами невалидных полей

console.log("Невалидные поля: " + bad.join(", "));
Ожидаемый результат
Невалидные поля: fullName, email, age
Подсказка 1

for (const field of form.elements) { ... } — перебирает все поля формы по очереди.

Подсказка 2

Внутри цикла: if (!field.checkValidity()) bad.push(field.name);

ЗАДАЧА 4.2

Почему невалидно

средняя

Напишите функцию reasonOf(input). Она смотрит на флаги validity и возвращает причину: "пусто" (valueMissing), "не email" (typeMismatch), "слишком мало" (rangeUnderflow), "не по шаблону" (patternMismatch) или "ок", если поле валидно.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <input id="a" required value="">
  <input id="b" type="email" value="ali.mail.tj">
  <input id="c" type="number" min="16" value="12">
  <input id="d" pattern="[0-9]{4}" value="12ab">
  <input id="e" required value="всё хорошо">`;

function reasonOf(input) {
  // верните одну из пяти строк
}

for (const id of ["a", "b", "c", "d", "e"]) {
  console.log(id + ": " + reasonOf(document.getElementById(id)));
}
Ожидаемый результат
a: пусто
b: не email
c: слишком мало
d: не по шаблону
e: ок
Подсказка 1

Флаги лежат в input.validity. Проверяйте по очереди: if (input.validity.valueMissing) return "пусто";

Подсказка 2

После четырёх проверок с ранним return последняя строка функции — return "ок";

ЗАДАЧА 4.3

Валидна ли форма целиком

лёгкая

В форме есть обязательное поле «Имя» и корректный email. Выведите результат checkValidity() формы до заполнения имени, затем впишите имя «Мадина» и выведите результат ещё раз.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <form>
    <input name="fullName" required>
    <input name="email" type="email" value="madina@mail.tj">
  </form>`;

const form = document.querySelector("form");

// 1) выведите form.checkValidity()
// 2) впишите имя в поле fullName
// 3) выведите form.checkValidity() ещё раз
Ожидаемый результат
false
true
Подсказка 1

У самой формы есть тот же метод, что и у поля: form.checkValidity() — он проверяет все поля сразу.

Подсказка 2

Значение задаётся так: form.elements.fullName.value = "Мадина";

05 Регулярные выражения для новичков

Регулярное выражение (regex, «регэксп») — это короткий шаблон, который описывает, как должна выглядеть строка. «Только цифры», «начинается с +992 и дальше девять цифр», «латинские буквы и подчёркивание» — всё это записывается одной строчкой. Для проверки форм это незаменимый инструмент.

Аналогия из жизни

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

Можно редактировать · Ctrl + Enter
const onlyDigits = /^\d+$/;          // между слэшами — шаблон

console.log(onlyDigits.test("12345"));  // true  — подходит
console.log(onlyDigits.test("12a45"));  // false — есть буква
console.log(onlyDigits.test(""));       // false — пусто (нужна хотя бы одна цифра)

Регулярное выражение записывается между двумя слэшами /.../. Метод .test(строка) возвращает true, если строка подходит под шаблон, иначе — false. Теперь разберём «буквы» этого языка.

Алфавит регулярок: всего несколько символов

ЗаписьЗначениеПример
abcОбычные символы значат сами себя/tj/ найдёт «tj»
\dОдна цифра (0–9)/\d/ подходит к «a1»
\DОдин символ, который НЕ цифра/\D/ подходит к «a1»
\sПробельный символ: пробел, таб, перевод строки/\s/ подходит к «a b»
.Любой один символ/a.c/ подходит к «abc», «a-c»
[abc]Один символ из списка/[аеи]/ — одна из трёх гласных
[a-z], [0-9]Один символ из диапазона/[a-zA-Z]/ — любая латинская буква
[^abc]Один символ, которого НЕТ в списке/[^\s@]/ — не пробел и не «@»
^ и $Начало и конец всей строки/^\d+$/ — вся строка из цифр
+Предыдущее — один раз или больше/\d+/ — число из любого количества цифр
*Предыдущее — ноль раз или больше/ab*/ — «a», «ab», «abbb»
?Предыдущее — один раз или ни разу/\+?\d+/ — плюс не обязателен
{4}, {3,16}Ровно 4 раза / от 3 до 16 раз/\d{9}/ — девять цифр
\+, \.Слэш перед спецсимволом: ищем сам символ\. — именно точка, а не «любой символ»

Читаем регулярку по частям

Номер телефона в Таджикистане: +992 и девять цифр. Разберём шаблон слева направо:

^ \+992 \d{9} $
/^\+992\d{9}$/ — строка начинается с «+992», дальше ровно девять цифр, и больше ничего
Можно редактировать · Ctrl + Enter
const phone = /^\+992\d{9}$/;

console.log(phone.test("+992927771234"));    // true
console.log(phone.test("+99292777123"));     // false — только 8 цифр
console.log(phone.test("+9929277712345"));   // false — 10 цифр, лишняя
console.log(phone.test("927771234"));        // false — нет кода страны

Якоря ^ и $: самая частая ошибка

Без ^ и $ регулярка ищет подходящий кусочек внутри строки — и пропускает мусор вокруг:

Баг: забыли якоря · Ctrl + Enter
const bad  = /\d{4}/;      // «где-то в строке есть 4 цифры подряд»
const good = /^\d{4}$/;    // «вся строка — ровно 4 цифры»

console.log(bad.test("abc1234xyz"));   // true  ← ✖ пропустили мусор
console.log(good.test("abc1234xyz"));  // false ← так правильно
console.log(good.test("1234"));        // true

Флаги и замена: чистим ввод

После закрывающего слэша можно ставить флаги: i — не различать большие и маленькие буквы, g — искать все совпадения, а не только первое. С методом replace это удобный способ почистить телефон от пробелов и дефисов:

Можно редактировать · Ctrl + Enter
const raw = "+992 92 777-12-34";
const clean = raw.replace(/[\s-]/g, "");   // убираем все пробелы и дефисы
console.log(clean);                        // +992927771234

console.log(/^tj$/i.test("TJ"));           // true — регистр не важен из-за флага i
console.log("a1b22c333".replace(/\D/g, ""));  // 122333 — оставили только цифры

Готовые шаблоны для форм

Что проверяемРегуляркаКак читать
Телефон +992/^\+992\d{9}$/«+992» и ровно 9 цифр
Логин/^[a-zA-Z][a-zA-Z0-9_]{2,15}$/первая — латинская буква, дальше 2–15 букв, цифр или «_»: всего 3–16 символов
Email (простой)/^[^\s@]+@[^\s@]+\.[^\s@]+$/что-то без пробелов и «@», затем «@», затем снова такое же, затем точка и ещё что-то
Есть цифра/\d/где-то в строке встречается цифра (якоря не нужны!)
Есть заглавная буква/[A-Z]/где-то есть латинская заглавная
Имя на кириллице/^[А-Яа-яЁё\s-]+$/только русские буквы (Ё и ё нужно добавить отдельно), пробелы и дефис
Что нужно знать про регулярки
  • Якоря. Проверяете «вся строка такая» — ставьте ^ и $. Ищете «содержит» — не ставьте.
  • Не угадывайте, проверяйте. Регулярка — это код. Прогоните её на 5–10 примерах: подходящих и нет (в разделе про ИИ мы сделаем это системно).
  • Не усложняйте. Идеального выражения для email не существует, а «идеальные» занимают три строки, и никто их не понимает. Достаточно простой проверки формата. Что адрес реально существует, проверяют письмом с кодом, а не регуляркой.
  • Кириллица и \w. Шаблон \w (буквы, цифры, «_») понимает только латиницу. Для русских и таджикских букв пишите диапазоны явно и проверяйте на реальных именах.

Читать подробнее: Шаблоны и флаги · Якоря: начало ^ и конец $ · MDN: регулярные выражения

Практика: закрепляем

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

ЗАДАЧА 5.1

Только цифры

лёгкая

Замените тройку подчёркиваний на шаблон «вся строка состоит минимум из одной цифры».

Можно редактировать · Ctrl + Enter
const onlyDigits = /___/;

for (const s of ["12345", "12a45", "", "007"]) {
  console.log("[" + s + "] → " + onlyDigits.test(s));
}
Ожидаемый результат
[12345] → true
[12a45] → false
[] → false
[007] → true
Подсказка 1

Нужны якоря начала и конца строки и «цифра, один раз или больше».

Подсказка 2

/^\d+$/

ЗАДАЧА 5.2

Телефон с пробелами

средняя

Напишите функцию isPhone(value): сначала она убирает из строки все пробелы и дефисы, затем проверяет, что получился номер вида +992 и девять цифр.

Можно редактировать · Ctrl + Enter
function isPhone(value) {
  // 1) очистите value   2) проверьте шаблон
}

const tests = ["+992927771234", "+992 92 777-12-34", "927771234", "+99292777123", "+992abc771234", ""];
for (const t of tests) {
  console.log("[" + t + "] → " + isPhone(t));
}
Ожидаемый результат
[+992927771234] → true
[+992 92 777-12-34] → true
[927771234] → false
[+99292777123] → false
[+992abc771234] → false
[] → false
Подсказка 1

Очистка: value.replace(/[\s-]/g, "") — флаг g нужен, чтобы убрать все пробелы, а не только первый.

Подсказка 2

Шаблон — из раздела выше: /^\+992\d{9}$/. Функция возвращает результат .test(...).

ЗАДАЧА 5.3

Правила для логина

средняя

Напишите isLogin(value). Логин подходит, если он состоит только из латинских букв, цифр и «_», начинается с буквы и имеет длину от 3 до 16 символов.

Можно редактировать · Ctrl + Enter
function isLogin(value) {
  // верните true или false
}

const tests = ["madina_01", "Rustam", "1madina", "ma", "madina-01", "Shahrom_Komilov_2024_x"];
for (const t of tests) {
  console.log(t + " → " + isLogin(t));
}
Ожидаемый результат
madina_01 → true
Rustam → true
1madina → false
ma → false
madina-01 → false
Shahrom_Komilov_2024_x → false
Подсказка 1

Шаблон из двух частей: первая буква [a-zA-Z], остальные — [a-zA-Z0-9_] с количеством. Сколько «остальных» должно быть, если вся длина 3–16?

Подсказка 2

return /^[a-zA-Z][a-zA-Z0-9_]{2,15}$/.test(value);

ЗАДАЧА 5.4

Найди баг: индекс из шести цифр

лёгкая

Почтовый индекс — ровно шесть цифр. Регулярка пропускает лишнее. Исправьте её, чтобы все четыре строки дали правильный ответ.

Можно редактировать · Ctrl + Enter
const zip = /\d{6}/;

for (const s of ["734025", "73402", "7340256", "ab734025"]) {
  console.log(s + " → " + zip.test(s));
}
Ожидаемый результат
734025 → true
73402 → false
7340256 → false
ab734025 → false
Подсказка 1

Сейчас шаблон ищет шесть цифр где-то внутри строки. Нужно сказать, что строка состоит только из них.

Подсказка 2

Добавьте якоря: /^\d{6}$/

ЗАДАЧА 5.5

Сила пароля

сложная

Напишите passwordStrength(p). Пароль получает по баллу за каждое выполненное условие: длина от 8 символов; есть строчная латинская буква; есть заглавная латинская буква; есть цифра; есть спецсимвол из набора !@#$%^&*. Результат: 0–2 балла — слабый, 3–4 — средний, 5 — сильный.

Можно редактировать · Ctrl + Enter
function passwordStrength(p) {
  let points = 0;
  // добавляйте к points по 1 за каждое условие

  // верните "слабый", "средний" или "сильный"
}

for (const p of ["", "abc", "12345678", "madina123", "Madina2024", "Madina2024!"]) {
  console.log("[" + p + "] → " + passwordStrength(p));
}
Ожидаемый результат
[] → слабый
[abc] → слабый
[12345678] → слабый
[madina123] → средний
[Madina2024] → средний
[Madina2024!] → сильный
Подсказка 1

Пять отдельных if: if (p.length >= 8) points++;, if (/[a-z]/.test(p)) points++; и так далее. Здесь якоря не нужны — мы ищем «содержит».

Подсказка 2

Спецсимволы: /[!@#$%^&*]/. В конце: if (points <= 2) return "слабый"; if (points <= 4) return "средний"; return "сильный";

06 Своя валидация: функции-валидаторы и тексты ошибок

Теперь у нас есть всё, чтобы написать собственную проверку. Рецепт простой и работает для любой формы:

  1. Один валидатор на поле. Это функция, которая принимает значение и возвращает текст ошибки — или пустую строку "", если всё хорошо.
  2. Собираем в объект. { fullName: validateName, email: validateEmail, ... } — по ключу видно, какой валидатор к какому полю.
  3. Показываем ошибки. Текст — в <span> под полем, а само поле помечаем красной рамкой и атрибутом aria-invalid.
  4. Не отправляем, пока есть хоть одна ошибка.

Шаг 1: валидатор — это функция «значение → текст ошибки»

Можно редактировать · Ctrl + Enter
function validateEmail(value) {
  const email = value.trim();                       // убираем пробелы по краям
  if (email === "") return "Введите email";
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    return "Похоже, в адресе ошибка. Пример: madina@mail.tj";
  }
  return "";                                        // пустая строка = ошибок нет
}

console.log("[" + validateEmail("") + "]");
console.log("[" + validateEmail("madina@") + "]");
console.log("[" + validateEmail("  madina@mail.tj ") + "]");

Функция только проверяет и ничего не знает про страницу. Поэтому её легко тестировать (мы только что вызвали её с тремя примерами) и легко использовать повторно — на любой странице и с любым дизайном. Правило «функция считает, другая функция показывает» вы уже встречали на уроке 5.

Аналогия из жизни

Контролёр на входе в метро. Он не бежит за вами — он стоит у турникета и смотрит на билет. Если что-то не так, он называет причину («приложите карту», «не хватает денег»). Валидатор — тот же контролёр: на выходе либо «проходите» (""), либо понятная причина отказа.

Какие тексты ошибок писать

Плохо

«Ошибка!», «Неверное значение», «Invalid input», «Поле заполнено неверно». Человек не понимает, что не так и как исправить.

Хорошо

«Введите номер в формате +992 и 9 цифр», «Пароль — минимум 8 символов», «Пароли не совпадают». Сказано, чего ждём, и сразу виден пример.

  • Говорите на «вы» и спокойно: не «Ты ввёл неправильно», а «Проверьте, пожалуйста, email».
  • Называйте правило, а не только факт ошибки: «минимум 2 буквы» полезнее, чем «слишком коротко».
  • Показывайте ошибку рядом с полем, а не одним окном alert вверху страницы.
  • Пустое обязательное поле и неверный формат — это разные сообщения: «Введите email» и «Неверный формат email».

Шаг 2: проверяем все поля сразу

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

Можно редактировать · Ctrl + Enter
const validators = {
  fullName: (v) => (v.trim().length < 2 ? "Введите имя (минимум 2 буквы)" : ""),
  age: (v) => (Number(v) >= 16 ? "" : "Курс с 16 лет"),
};

function validateAll(data) {
  const errors = {};
  for (const field in validators) {
    const message = validators[field](data[field]);
    if (message) errors[field] = message;
  }
  return errors;
}

console.log(JSON.stringify(validateAll({ fullName: "Мадина", age: "19" })));   // {}
console.log(JSON.stringify(validateAll({ fullName: "М", age: "15" })));

Шаг 3: показываем ошибку на странице

Каждое поле получает «место для ошибки» — пустой <span> рядом. При ошибке мы пишем в него текст, при её отсутствии — стираем. Всегда используйте textContent, а не innerHTML: ввод пользователя нельзя вставлять в страницу как HTML.

Ошибки под полями · Ctrl + Enter
<form id="f" novalidate>
  <div class="field">
    <label for="fullName">Имя</label>
    <input id="fullName" name="fullName">
    <span class="error" data-error="fullName"></span>
  </div>
  <div class="field">
    <label for="age">Возраст</label>
    <input id="age" name="age" inputmode="numeric">
    <span class="error" data-error="age"></span>
  </div>
  <button>Отправить</button>
</form>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Запустите, нажмите «Отправить» с пустыми полями, затем исправьте и отправьте снова. Обратите внимание на три детали: ошибки показываются сразу у всех полей, курсор встаёт в первое «плохое» поле, а у исправленного поля ошибка пропадает.

Типичные ошибки
  • Забыли trim() — « » (пробелы) считается непустым именем.
  • Сравниваем строку как число — "5" < "16" сравнит строки по символам. Сначала Number(...).
  • Не стираем старую ошибку — человек исправил поле, а красный текст остался. Поэтому showError всегда вызывается и для валидных полей — с пустой строкой.
  • Показываем ввод через innerHTML — открывает дыру для XSS. Только textContent.
  • Проверка только в браузере — для настоящего сайта нужна ещё и серверная.

Читать подробнее: Фокусировка: focus/blur · MDN: валидация форм с помощью JavaScript

Практика: закрепляем

Пишем валидаторы — от одного поля до целой формы. Выводимые тексты ошибок сверяются буква в букву, поэтому копируйте их из условия.

ЗАДАЧА 6.1

Валидатор имени

лёгкая

Напишите validateName(value). Пробелы по краям игнорируются. Пустое имя → Введите имя. Короче двух символов → Имя слишком короткое. Иначе — пустая строка.

Можно редактировать · Ctrl + Enter
function validateName(value) {
  // верните "" или текст ошибки
}

for (const v of ["", "   ", "А", "Мадина", "  Фарход  "]) {
  console.log("[" + validateName(v) + "]");
}
Ожидаемый результат
[Введите имя]
[Введите имя]
[Имя слишком короткое]
[]
[]
Подсказка 1

Сначала const name = value.trim(); — дальше работаем с очищенной строкой.

Подсказка 2

Две проверки с ранним return: if (name === "") return "Введите имя"; и if (name.length < 2) return .... В конце — return "";

ЗАДАЧА 6.2

Валидатор возраста

средняя

Напишите validateAge(value) — значение приходит строкой из поля. Правила в таком порядке: пустое (после trim) → Введите возраст; не число → Возраст — это число; меньше 16 → Курс с 16 лет; больше 70 → Проверьте возраст; иначе "".

Можно редактировать · Ctrl + Enter
function validateAge(value) {
  // не забудьте: Number("") равно 0, поэтому пустоту проверяем отдельно и первой
}

for (const v of ["", "abc", "15", "24", "99", " 30 "]) {
  console.log("[" + validateAge(v) + "]");
}
Ожидаемый результат
[Введите возраст]
[Возраст — это число]
[Курс с 16 лет]
[]
[Проверьте возраст]
[]
Подсказка 1

Порядок: const text = value.trim(); → пусто → const age = Number(text); → Number.isNaN(age) → границы.

Подсказка 2

Number("abc") даёт NaN, а проверить его можно только через Number.isNaN(age) (сравнение age === NaN всегда false).

ЗАДАЧА 6.3

Все проблемы пароля сразу

средняя

Напишите validatePassword(password, login), которая возвращает массив всех найденных проблем (в этом порядке): Минимум 8 символов; Добавьте цифру (нет ни одной цифры); Без пробелов (есть пробельный символ); Не совпадает с логином (пароль равен логину без учёта регистра). Если проблем нет — пустой массив.

Можно редактировать · Ctrl + Enter
function validatePassword(password, login) {
  const problems = [];
  // добавляйте тексты в problems через push
  return problems;
}

const tests = [["abc", "madina"], ["Madina", "madina"], ["pass word 1", "x"], ["qwerty123", "madina"]];
for (const [p, l] of tests) {
  console.log(validatePassword(p, l).join(" | ") || "ок");
}
Ожидаемый результат
Минимум 8 символов | Добавьте цифру
Минимум 8 символов | Добавьте цифру | Не совпадает с логином
Без пробелов
ок
Подсказка 1

Четыре независимых if без return: каждый при необходимости добавляет свой текст в массив.

Подсказка 2

Цифра: !/\d/.test(password). Пробел: /\s/.test(password). Логин: password.toLowerCase() === login.toLowerCase().

ЗАДАЧА 6.4

Проверяем форму целиком

средняя

Три валидатора уже написаны. Напишите validateForm(data): она вызывает нужный валидатор для каждого поля из объекта validators и возвращает объект ошибок — с ключами только для невалидных полей.

Можно редактировать · Ctrl + Enter
function validateName(v) {
  return v.trim().length < 2 ? "Введите имя" : "";
}
function validateEmail(v) {
  if (v.trim() === "") return "Введите email";
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) ? "" : "Неверный email";
}
function validatePhone(v) {
  return /^\+992\d{9}$/.test(v.replace(/[\s-]/g, "")) ? "" : "Телефон: +992 и 9 цифр";
}

const validators = { fullName: validateName, email: validateEmail, phone: validatePhone };

function validateForm(data) {
  // верните объект ошибок
}

const forms = [
  { fullName: "Мадина", email: "madina@mail.tj", phone: "+992 92 777 12 34" },
  { fullName: " ", email: "madina@", phone: "+992927771234" },
  { fullName: "Фарход", email: "", phone: "927771234" },
];
for (const data of forms) console.log(JSON.stringify(validateForm(data)));
Ожидаемый результат
{}
{"fullName":"Введите имя","email":"Неверный email"}
{"email":"Введите email","phone":"Телефон: +992 и 9 цифр"}
Подсказка 1

Создайте const errors = {};, пройдите циклом for (const field in validators) и в конце верните errors.

Подсказка 2

Внутри цикла: const message = validators[field](data[field]); и if (message) errors[field] = message;

ЗАДАЧА 6.5

Ошибки на странице

сложная

В скрипте не хватает двух частей. Напишите showError(name, message): она пишет текст в span[data-error="имя"], ставит или снимает класс invalid у поля и выставляет aria-invalid в "true" или "false". Затем напишите обработчик submit: отмена отправки, проверка всех полей через validators, показ ошибок и при успехе console.log("Отправлено"). Блок «проверка» внизу не меняйте.

Допишите showError и submit · Ctrl + Enter
<form id="f" novalidate>
  <div class="field">
    <label for="fullName">Имя</label>
    <input id="fullName" name="fullName">
    <span class="error" data-error="fullName"></span>
  </div>
  <div class="field">
    <label for="email">Email</label>
    <input id="email" name="email">
    <span class="error" data-error="email"></span>
  </div>
  <button>Отправить</button>
</form>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
fullName: Введите имя (минимум 2 буквы) | aria-invalid=true | invalid=true
email: ok | aria-invalid=false | invalid=false
fullName: ok | aria-invalid=false | invalid=false
email: Введите email, например madina@mail.tj | aria-invalid=true | invalid=true
Отправлено
fullName: ok | aria-invalid=false | invalid=false
email: ok | aria-invalid=false | invalid=false
Подсказка 1

showError: найдите поле form.elements[name] и подпись form.querySelector(`[data-error="${name}"]`); запишите textContent; classList.toggle("invalid", message !== ""); setAttribute("aria-invalid", message ? "true" : "false").

Подсказка 2

В обработчике: event.preventDefault(), let ok = true;, цикл for (const name in validators) — внутри считаем сообщение, вызываем showError (даже если сообщение пустое!) и при ошибке ставим ok = false. После цикла: if (ok) console.log("Отправлено");

07 Живая валидация: проверяем, пока человек печатает

Проверка только при нажатии «Отправить» работает, но неудобно: человек заполнил десять полей и узнаёт об ошибке в самом первом. Живая валидация подсказывает сразу. Для неё нужны три события:

input
Срабатывает на каждое изменение значения: нажали клавишу, вставили текст, стёрли символ.
change
Срабатывает, когда значение поменялось и поле потеряло фокус (для чекбокса и списка — сразу при выборе).
blur
Поле потеряло фокус — человек ушёл в другое поле. Не всплывает (в отличие от focusout).
Аналогия из жизни

Хороший официант в чайхоне не перебивает вас на полуслове: «Нет, плова уже нет!» Он ждёт, пока вы закончите говорить, и только потом поправляет. Так и форма: не ругайтесь на поле, пока человек не закончил его вводить, иначе первая же буква email вызовет красное «Неверный email».

Правило «тронуто / не тронуто» (touched)

  1. Пока человек не покидал поле — ошибку не показываем, даже если значение пока неверное.
  2. Когда сработал blur — поле считается «тронутым»: проверяем и показываем ошибку.
  3. С этого момента на каждый input перепроверяем: как только человек исправил, ошибка исчезает сразу.
  4. При нажатии «Отправить» «трогаем» все поля сразу и показываем все ошибки.
Живая проверка email и пароля · Ctrl + Enter
<form id="f" novalidate>
  <div class="field">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" autocomplete="off">
    <span class="error" id="email-error"></span>
  </div>
  <div class="field">
    <label for="pass">Пароль</label>
    <div class="row">
      <input id="pass" name="pass" type="password" maxlength="20">
      <button type="button" id="eye">Показать</button>
    </div>
    <span class="hint" id="pass-hint">Введено: 0 из 20</span>
  </div>
</form>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.

Попробуйте: наберите в email «ali» — ничего не происходит. Нажмите Tab — появилась ошибка. Допишите «@mail.tj» — ошибка исчезла прямо во время набора.

Сравнение двух полей: пароль и повтор

Правило «пароли совпадают» зависит от двух полей, поэтому проверять нужно при изменении любого из них. Частая ошибка — подписаться только на второе поле: человек поправил первый пароль, а красная ошибка у второго осталась.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `<input id="pass"><input id="again"><span id="msg"></span>`;
const pass = document.querySelector("#pass");
const again = document.querySelector("#again");
const msg = document.querySelector("#msg");

function checkMatch() {
  msg.textContent = pass.value === again.value ? "" : "Пароли не совпадают";
}
pass.addEventListener("input", checkMatch);    // оба поля влияют на результат
again.addEventListener("input", checkMatch);

pass.value = "qwerty1";   pass.dispatchEvent(new Event("input"));
again.value = "qwerty1";  again.dispatchEvent(new Event("input"));
console.log("[" + msg.textContent + "]");       // [] — совпали
pass.value = "qwerty2";   pass.dispatchEvent(new Event("input"));
console.log("[" + msg.textContent + "]");       // [Пароли не совпадают] — поправили первое
Типичные ошибки
  • Ругаемся слишком рано. Красная ошибка после первой буквы раздражает. Используйте «touched».
  • Проверяем только на change. Тогда человек видит ошибку только после ухода из поля — это уже не «живая» проверка. Основное событие — input.
  • Забыли проверку при отправке. Человек может нажать «Отправить», вообще не заходя в поле: живая валидация дополняет проверку на submit, а не заменяет её.
  • Блокируем кнопку без объяснения. Заблокированная кнопка «Отправить» без подсказки, чего не хватает, — плохой интерфейс. Лучше показывать ошибки и оставить кнопку активной.

Читать подробнее: События change, input · Фокусировка: focus/blur

Практика: закрепляем

В задачах скрипт сам «печатает» в поля и вызывает события (dispatchEvent) — вам нужно написать реакцию.

ЗАДАЧА 7.1

Показать пароль

лёгкая

По клику на кнопку тип поля переключается между password и text, а текст кнопки — между «Показать» и «Скрыть».

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `
  <input id="pass" type="password" value="secret">
  <button id="eye" type="button">Показать</button>`;

const pass = document.querySelector("#pass");
const eye = document.querySelector("#eye");

// добавьте обработчик click для кнопки eye


eye.click();
console.log(pass.type, eye.textContent);
eye.click();
console.log(pass.type, eye.textContent);
Ожидаемый результат
text Скрыть
password Показать
Подсказка 1

Внутри обработчика сначала узнайте, скрыт ли пароль: const hidden = pass.type === "password";

Подсказка 2

Тернарный оператор: pass.type = hidden ? "text" : "password"; и по аналогии для eye.textContent.

ЗАДАЧА 7.2

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

средняя

Под полем «О себе» живёт надпись «Осталось: N», где N — сколько символов ещё можно ввести (лимит 40). Она должна быть правильной сразу при загрузке и обновляться на каждое событие input.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `<textarea id="bio"></textarea><p id="left"></p>`;
const bio = document.querySelector("#bio");
const left = document.querySelector("#left");
const LIMIT = 40;

// 1) функция update() записывает в left текст «Осталось: N»
// 2) вызовите её сразу и на событие input


console.log(left.textContent);
bio.value = "Привет!";
bio.dispatchEvent(new Event("input"));
console.log(left.textContent);
bio.value = "а".repeat(40);
bio.dispatchEvent(new Event("input"));
console.log(left.textContent);
Ожидаемый результат
Осталось: 40
Осталось: 33
Осталось: 0
Подсказка 1

Длина текста — bio.value.length. Осталось: LIMIT - bio.value.length.

Подсказка 2

Объявите function update() { left.textContent = `Осталось: ${LIMIT - bio.value.length}`; }, вызовите update(); и добавьте bio.addEventListener("input", update);

ЗАДАЧА 7.3

Ругаемся только после ухода из поля

средняя

Реализуйте правило «touched» для поля email. На blur поле считается тронутым и проверяется. На input проверка идёт только если поле уже тронуто. Ошибка — Неверный email, а при корректном адресе текст ошибки пустой.

Можно редактировать · Ctrl + Enter
document.body.innerHTML = `<input id="email"><span id="error"></span>`;
const email = document.querySelector("#email");
const error = document.querySelector("#error");

// ваш код: переменная touched, функция validate, обработчики blur и input


function show() {
  console.log(error.textContent || "нет ошибки");
}

email.value = "ali";
email.dispatchEvent(new Event("input"));
show();
email.dispatchEvent(new Event("blur"));
show();
email.value = "ali@mail.tj";
email.dispatchEvent(new Event("input"));
show();
Ожидаемый результат
нет ошибки
Неверный email
нет ошибки
Подсказка 1

Нужна переменная let touched = false;. В обработчике blur ставим touched = true и вызываем validate().

Подсказка 2

validate() пишет в error.textContent либо "Неверный email" (если значение не подходит под /^[^\s@]+@[^\s@]+\.[^\s@]+$/), либо пустую строку. Обработчик input: if (touched) validate();

ЗАДАЧА 7.4

Кнопка зависит от двух полей

сложная

Кнопка «Создать аккаунт» должна быть активной только когда пароль не короче 8 символов и повтор пароля ему равен. Напишите updateButton(), вызовите её при загрузке и при вводе в любое из двух полей.

Пароль и повтор · Ctrl + Enter
<form id="f">
  <input id="password" type="password" placeholder="Пароль">
  <input id="confirm" type="password" placeholder="Повторите пароль">
  <button id="submit">Создать аккаунт</button>
</form>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
старт: кнопка заблокирована
пароль="1234", повтор="" → кнопка заблокирована
пароль="12345678", повтор="" → кнопка заблокирована
пароль="12345678", повтор="1234567" → кнопка заблокирована
пароль="12345678", повтор="12345678" → кнопка активна
пароль="87654321", повтор="12345678" → кнопка заблокирована
Подсказка 1

Условие готовности: password.value.length >= 8 && password.value === confirm.value. Кнопка заблокирована, когда оно ложно: submit.disabled = !ready;

Подсказка 2

Подпишитесь обеими строками: password.addEventListener("input", updateButton); и то же для confirm. Не забудьте вызвать updateButton(); один раз сразу.

08 ИИ-блок: правила валидации и регулярки

ИИ-подсказка

Регулярные выражения — одна из лучших задач для ИИ: писать их вручную долго, а объяснить «что хочу» словами легко. Но именно здесь ИИ чаще всего уверенно ошибается. Поэтому схема всегда одна: ИИ предлагает — вы проверяете на своих примерах.

Напомним правила курса: сначала пробуйте сами 15 минут, и никогда не вставляйте код, который не можете объяснить строка за строкой. Сегодня вы уже умеете читать регулярки, так что ответ ИИ можно разобрать по частям.

1. Просим правила валидации

Используем формулу промпта: контекст → задача → ограничения → формат ответа → проверка.

сделай валидацию формы регистрации
Контекст: я пишу на чистом JavaScript без библиотек, уровень — новичок. Делаю форму регистрации для курсов в Душанбе. Задача: составь правила валидации для полей: имя, телефон (Таджикистан, +992), email, пароль, повтор пароля. Для каждого — условие, сообщение об ошибке на русском и регулярное выражение, если оно нужно. Ограничения: простые регулярки, которые новичок сможет прочитать; без внешних библиотек; сообщения — вежливые, с примером правильного ввода. Формат ответа: таблица «поле | правило | сообщение об ошибке | регулярка». Проверка: для каждой регулярки приведи по 3 строки, которые подходят, и 3, которые не подходят, и объясни регулярку по частям.

2. Проверяем регулярку от ИИ на своих кейсах

Не верьте на слово даже самому уверенному ответу. Заведите привычку — таблица тестов:

  1. 3–5 подходящих строк, включая «грязные» (с пробелами по краям, в разном регистре).
  2. Пустая строка — должна быть отвергнута (если поле обязательное).
  3. Границы длины: на единицу меньше минимума, ровно минимум, ровно максимум, на единицу больше.
  4. «Мусор» вокруг: abc1234xyz там, где нужны только цифры. Так вы ловите отсутствие ^ и $.
  5. Ваши настоящие данные: телефоны из вашей записной книжки, имена друзей на кириллице, ваш реальный email.
Мини-стенд для проверки регулярки · Ctrl + Enter
const re = /^\+992\d{9}$/;   // регулярка от ИИ

const cases = [
  ["+992927771234", true],      // подходит
  [" +992927771234", false],    // пробел в начале — вы решаете, допустимо ли
  ["+99292777123", false],      // на цифру меньше
  ["+9929277712345", false],    // на цифру больше
  ["abc+992927771234xyz", false],  // мусор вокруг
  ["", false],                  // пусто
];

for (const [value, expected] of cases) {
  console.log((re.test(value) === expected ? "ok  " : "FAIL") + " [" + value + "]");
}

3. Что ИИ делает неправильно чаще всего

Что видите в ответе ИИВ чём проблемаКак исправить
Регулярка без ^ и $Пропускает мусор вокруг: abc1234xyz проходит как «цифры»Добавить якоря, если проверяется вся строка
Email на 200 символов с кучей скобокНикто не сможет прочитать и поддерживать, при этом всё равно не проверяет реального существования адресаПростая /^[^\s@]+@[^\s@]+\.[^\s@]+$/
\w или [a-z] для имёнКириллические и таджикские буквы отвергаютсяЯвные диапазоны [А-Яа-яЁё] и тест на реальных именах
Телефон ^\d{10}$Шаблон под другую странуСказать ИИ в промпте: Таджикистан, +992, 9 цифр
<input type="number"> для телефонаТеряет ведущие нули и «+», у поля есть стрелочкиtype="tel" + проверка строкой
Регулярка с флагом g и метод .test()Объект «помнит» позицию: ответы чередуются true / falseУбрать флаг g для проверок
errorBox.innerHTML = userInputОпасно: пользователь сможет вставить HTMLerrorBox.textContent = ...
«Проверка на клиенте — это защита»Браузерную проверку легко обойтиПомнить: проверяет ещё и сервер
Ловушка флага g · Ctrl + Enter
const re = /\d/g;                  // флаг g — «помни, где остановился»
console.log(re.test("1"));         // true
console.log(re.test("1"));         // false ← ✖ та же строка, другой ответ
console.log(re.test("1"));         // true

4. Ещё три полезных промпта

Объясни регулярное выражение /^[a-zA-Z][a-zA-Z0-9_]{2,15}$/ по частям, как для новичка. Для каждой части — что она значит. Затем дай 3 строки, которые подходят, и 3, которые не подходят, с объяснением почему.
Вот моя функция validatePhone: [код]. Придумай 10 тестовых строк, на которых она может сломаться: пробелы, дефисы, буквы, пустая строка, слишком длинный номер. Ответ — массив пар [строка, ожидаемый результат] в виде JS-кода.
Перепиши мои сообщения об ошибках формы так, чтобы они были вежливыми, короткими и называли правило и пример. Не меняй смысл. Было: «Неверно», «Ошибка в пароле», «Плохой телефон».
Главное правило

Прочитайте регулярку от ИИ по частям, как мы делали в разделе 5. Если вы не можете объяснить какую-то часть — попросите ИИ объяснить и проверьте это тестами, либо упростите выражение. Код, который вы не понимаете, вы не сможете исправить, когда он сломается на реальных данных.

Практика с ИИ

Главное в этих задачах — не получить код, а проверить его. Первую задачу выполняйте в чате с ИИ, остальные — здесь.

ЗАДАЧА 8.1

Промпт для правил валидации

лёгкая

Составьте по формуле промпт для формы «Запись в футбольную секцию»: поля имя, возраст (от 7 до 17), телефон +992, email родителя, согласие. Попросите таблицу правил, сообщения об ошибках и регулярки с 3+3 тестовыми строками. Отправьте в ИИ, а полученные регулярки прогоните через мини-стенд из этого раздела. Запишите: какая регулярка не прошла ваши тесты и что вы исправили. Эта задача без автопроверки — отметьте её вручную, когда закончите.

Подсказка 1

Пять блоков промпта: контекст (JS, новичок, Душанбе), задача, ограничения (простые регулярки, вежливые сообщения), формат (таблица), проверка (тестовые строки).

Подсказка 2

Для возраста регулярка не нужна — это обычное сравнение чисел. Хороший ИИ так и ответит; если он предлагает регулярку для диапазона 7–17, это повод усомниться.

ЗАДАЧА 8.2

Проверка регулярки от ИИ: телефон

средняя

ИИ предложил регулярку для телефона Таджикистана. Запустите стенд и найдите, на каких примерах она ошибается, затем исправьте её, чтобы все строки получили ok и итог был Не прошли: 0. Список проверок не меняйте.

Можно редактировать · Ctrl + Enter
const phoneRe = /^\+992\d{9}/;   // регулярка от ИИ

const cases = [
  ["+992927771234", true],
  ["+992927771234567", false],
  ["+99292777123", false],
  ["992927771234", false],
  ["+992 927771234", false],
  ["+992abcdefghi", false],
  ["", false],
];

let failed = 0;
for (const [value, expected] of cases) {
  const ok = phoneRe.test(value) === expected;
  if (!ok) failed++;
  console.log((ok ? "ok" : "FAIL") + ": [" + value + "]");
}
console.log("Не прошли: " + failed);
Ожидаемый результат
ok: [+992927771234]
ok: [+992927771234567]
ok: [+99292777123]
ok: [992927771234]
ok: [+992 927771234]
ok: [+992abcdefghi]
ok: []
Не прошли: 0
Подсказка 1

Какой строке «лишние цифры в конце» удаётся проскочить? Шаблон проверяет начало строки, но ничего не говорит про конец.

Подсказка 2

Добавьте якорь конца строки: /^\+992\d{9}$/

ЗАДАЧА 8.3

Email от ИИ: чиним и доверяем тестам

сложная

Функцию isEmail написал ИИ. Она пропускает лишнее и не обрабатывает пробелы по краям. Исправьте её так, чтобы все восемь проверок прошли: адрес с пробелами по краям считается правильным (пробелы нужно обрезать), а адреса без домена с точкой, без имени, с пробелом внутри или с двумя «@» — неправильными. Список cases не меняйте.

Можно редактировать · Ctrl + Enter
const isEmail = (s) => /^.+@.+$/.test(s);   // версия от ИИ

const cases = [
  ["madina@mail.tj", true],
  ["  madina@mail.tj  ", true],
  ["m.karimova+tj@gmail.com", true],
  ["madina@", false],
  ["madina@mail", false],
  ["@mail.tj", false],
  ["ma dina@mail.tj", false],
  ["madina@@mail.tj", false],
];

let failed = 0;
for (const [value, expected] of cases) {
  const ok = isEmail(value) === expected;
  if (!ok) failed++;
  console.log((ok ? "ok" : "FAIL") + ": [" + value + "]");
}
console.log("Не прошли: " + failed);
Ожидаемый результат
ok: [madina@mail.tj]
ok: [  madina@mail.tj  ]
ok: [m.karimova+tj@gmail.com]
ok: [madina@]
ok: [madina@mail]
ok: [@mail.tj]
ok: [ma dina@mail.tj]
ok: [madina@@mail.tj]
Не прошли: 0
Подсказка 1

Точка . в регулярке — «любой символ», в том числе пробел и «@». Для частей адреса нужно «любой символ, кроме пробела и @»: [^\s@]+.

Подсказка 2

(s) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s.trim()) — обрезаем пробелы перед проверкой и требуем точку в домене.

09 Практика на уроке: форма регистрации с живой валидацией

Соберём всё вместе в одну форму — такую, какую вы увидите на любом сайте. Мы строим регистрацию на курс: имя, email, пароль и повтор пароля.

Правила нашей формы
  • Имя — минимум 2 символа без пробелов по краям. Ошибка: «Имя — минимум 2 буквы».
  • Email — простой формат что-то@что-то.что-то. Ошибка: «Введите email в формате name@mail.tj».
  • Пароль — минимум 8 символов, затем минимум одна цифра. Ошибки: «Пароль — минимум 8 символов» и «Добавьте хотя бы одну цифру».
  • Повтор пароля — совпадает с паролем. Ошибка: «Пароли не совпадают».
  • Ошибки появляются после ухода из поля (blur), дальше обновляются на каждый ввод (input).
  • При отправке проверяются все поля сразу, курсор встаёт в первое ошибочное, а при успехе выводится «Регистрация: имя, email» (пароль в логи не пишем — никогда!).

План работы

  1. Разметка. Форма с novalidate, у каждого поля — label, name и пустой span.error под ним (data-error="имя поля").
  2. Валидаторы. Функции «значение → текст ошибки», собранные в объект validators (раздел 6).
  3. Показ ошибок. showError — текст, красная рамка, aria-invalid.
  4. Живая проверка. Словарь touched, обработчики blur и input (раздел 7).
  5. Отправка. Обработчик submit: «трогаем» все поля, проверяем, фокус на первое ошибочное или успех (раздел 2).

Разметку, стили и общий «каркас» (шаги 1, 3) мы даём готовыми. Ваша работа — два этапа.

Практика: собираем форму

ЗАДАЧА 9.1

Этап 1: валидаторы и живая проверка

средняя

Каркас живой проверки уже работает: showError, validateField и обработчики blur / input написаны. Допишите три валидатора по правилам выше. Сообщения копируйте дословно.

Регистрация: этап 1 · Ctrl + Enter
<form id="reg" novalidate>
  <div class="field">
    <label for="fullName">Имя</label>
    <input id="fullName" name="fullName" autocomplete="off">
    <span class="error" data-error="fullName"></span>
  </div>
  <div class="field">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" autocomplete="off">
    <span class="error" data-error="email"></span>
  </div>
  <div class="field">
    <label for="password">Пароль</label>
    <input id="password" name="password" type="password">
    <span class="error" data-error="password"></span>
  </div>
  <div class="field">
    <label for="confirm">Повторите пароль</label>
    <input id="confirm" name="confirm" type="password">
    <span class="error" data-error="confirm"></span>
  </div>
  <button>Создать аккаунт</button>
</form>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
имя до blur: ok
имя после blur: Имя — минимум 2 буквы
имя после правки: ok
email: Введите email в формате name@mail.tj
email после правки: ok
пароль 1: Добавьте хотя бы одну цифру
пароль 2: Пароль — минимум 8 символов
пароль 3: ok
Подсказка 1

Имя: return value.trim().length < 2 ? "Имя — минимум 2 буквы" : ""; Email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()) ? "" : "Введите email в формате name@mail.tj".

Подсказка 2

Пароль — две проверки с ранним return: if (value.length < 8) return "Пароль — минимум 8 символов";, затем if (!/\d/.test(value)) return "Добавьте хотя бы одну цифру";, в конце return "";

ЗАДАЧА 9.2

Этап 2: повтор пароля и отправка

сложная

Три валидатора из этапа 1 уже готовы. Добавьте: (1) валидатор confirm — «Пароли не совпадают»; (2) перепроверку confirm при изменении пароля, если confirm уже «тронут»; (3) обработчик submit: отмена отправки, «трогаем» и проверяем все поля, при ошибке ставим фокус на первое ошибочное поле, иначе выводим Регистрация: имя, email (без пароля).

Регистрация: этап 2 · Ctrl + Enter
<form id="reg" novalidate>
  <div class="field">
    <label for="fullName">Имя</label>
    <input id="fullName" name="fullName" autocomplete="off">
    <span class="error" data-error="fullName"></span>
  </div>
  <div class="field">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" autocomplete="off">
    <span class="error" data-error="email"></span>
  </div>
  <div class="field">
    <label for="password">Пароль</label>
    <input id="password" name="password" type="password">
    <span class="error" data-error="password"></span>
  </div>
  <div class="field">
    <label for="confirm">Повторите пароль</label>
    <input id="confirm" name="confirm" type="password">
    <span class="error" data-error="confirm"></span>
  </div>
  <button>Создать аккаунт</button>
</form>
Предпросмотр
Нажмите «Запустить», чтобы увидеть результат.
Ожидаемый результат
повтор: Пароли не совпадают
повтор после правки: ok
после смены пароля: Пароли не совпадают
Регистрация: Мадина, madina@mail.tj
Подсказка 1

(1) confirm(value) { return value === form.elements.password.value ? "" : "Пароли не совпадают"; }, (2) form.elements.password.addEventListener("input", () => { if (touched.confirm) validateField("confirm"); });

Подсказка 2

Submit: event.preventDefault(); затем let firstBad = null; и цикл по Object.keys(validators): touched[name] = true; if (!validateField(name) && !firstBad) firstBad = form.elements[name];. Если firstBad есть — firstBad.focus(); return;. Иначе — const data = Object.fromEntries(new FormData(form)); и вывод имени и email.

Эталонное решение (откройте только после своей попытки)
JavaScript
const form = document.querySelector("#reg");
const touched = {};

const validators = {
  fullName(value) {
    return value.trim().length < 2 ? "Имя — минимум 2 буквы" : "";
  },
  email(value) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()) ? "" : "Введите email в формате name@mail.tj";
  },
  password(value) {
    if (value.length < 8) return "Пароль — минимум 8 символов";
    if (!/\d/.test(value)) return "Добавьте хотя бы одну цифру";
    return "";
  },
  confirm(value) {
    return value === form.elements.password.value ? "" : "Пароли не совпадают";
  },
};

function showError(name, message) {
  const input = form.elements[name];
  form.querySelector(`[data-error="${name}"]`).textContent = message;
  input.classList.toggle("invalid", message !== "");
  input.setAttribute("aria-invalid", message ? "true" : "false");
}

function validateField(name) {
  const message = validators[name](form.elements[name].value);
  showError(name, message);
  return message === "";
}

for (const name of Object.keys(validators)) {
  const input = form.elements[name];
  input.addEventListener("blur", () => {
    touched[name] = true;
    validateField(name);
  });
  input.addEventListener("input", () => {
    if (touched[name]) validateField(name);
  });
}

form.elements.password.addEventListener("input", () => {
  if (touched.confirm) validateField("confirm");
});

form.addEventListener("submit", (event) => {
  event.preventDefault();
  let firstBad = null;
  for (const name of Object.keys(validators)) {
    touched[name] = true;
    if (!validateField(name) && !firstBad) firstBad = form.elements[name];
  }
  if (firstBad) {
    firstBad.focus();
    return;
  }
  const data = Object.fromEntries(new FormData(form));
  console.log(`Регистрация: ${data.fullName}, ${data.email}`);
});

// Песочница блокирует настоящую отправку формы, поэтому клик по кнопке превращаем в submit вручную
// (на обычной странице эта вставка не нужна)
form.querySelector("button").addEventListener("click", (event) => {
  event.preventDefault();
  form.dispatchEvent(new Event("submit", { cancelable: true }));
});
ИИ-проверка вашей формы

Когда ваш код работает, попросите ИИ придумать для него тесты: «Вот моя форма регистрации и валидаторы: [код]. Придумай 10 входных значений для каждого поля, на которых проверка может ошибиться (пробелы, пустое, слишком длинное, спецсимволы). Для каждого — ожидаемый результат». Прогоните эти значения через свои валидаторы и найдите расхождения.

★ Домашнее задание: форма из 6 полей

Сделайте форму регистрации на курс «Frontend + AI» с шестью полями, полной валидацией и понятными текстами ошибок. Начинайте с классной работы: она уже содержит каркас, вам нужно расширить его и довести до аккуратного результата.

Поля формы

#ПолеПравилоПример текста ошибки
1Имя и фамилияМинимум 2 слова, каждое от 2 букв, только буквы, пробелы и дефис«Введите имя и фамилию, например: Мадина Каримова»
2ТелефонФормат +992 и 9 цифр; пробелы и дефисы при вводе допускаются«Номер в формате +992 92 777 12 34»
3EmailПростой формат с «@» и точкой в домене«Введите email, например madina@mail.tj»
4ПарольОт 8 символов, есть цифра, есть заглавная буква, нет пробеловПокажите все нарушенные правила списком или через « · »
5Повтор пароляСовпадает с паролем (перепроверяется при смене пароля)«Пароли не совпадают»
6Согласие с правилами курсаЧекбокс должен быть отмечен«Без согласия записаться нельзя»

Что должно работать

  • ☐ Разметка: form novalidate, у каждого поля есть label, name и правильный type (tel, email, password, checkbox).
  • ☐ Каждый валидатор — отдельная функция «значение → текст ошибки или пустая строка».
  • ☐ Живая валидация по схеме «touched»: ошибка появляется после ухода из поля и пропадает сразу при исправлении.
  • ☐ При отправке проверяются все поля, фокус ставится на первое ошибочное.
  • ☐ Ошибки показываются под полями (textContent), у поля есть красная рамка и aria-invalid.
  • ☐ При успехе: на странице появляется сообщение «Спасибо, Имя! Мы свяжемся с вами по номеру ...», данные собраны через FormData, форма сбрасывается. Пароль нигде не показывается и не выводится в консоль.
  • ☐ Бонус: кнопка «Показать пароль» и счётчик оставшихся символов — любой один из них.

Что сдать

  1. Ссылка на репозиторий GitHub (папка lesson-10 с файлами index.html, style.css, script.js) или архив в Telegram-группу.
  2. Три скриншота: пустая отправка (все ошибки), частично заполненная форма, успешная отправка.
  3. Файл tests.md: таблица минимум из 10 тестовых значений для телефона и email — «значение → ожидаемый результат» — и отметка, какие ваш код прошёл.
  4. Если вы использовали ИИ: в README три строки — что именно просили, что взяли, что исправили руками. На проверке преподаватель может попросить объяснить любую строку.

Как оцениваем (0–10)

КритерийБаллы
Разметка: все 6 полей, label, name, type, novalidate1
Валидаторы имени, телефона и email работают на плохих и хороших данных3
Пароль (все правила) и повтор пароля (в том числе при смене пароля)2
Чекбокс согласия1
Живая валидация (touched), без «ругани» с первой буквы1
Отправка: все ошибки сразу, фокус на первом, успех, сброс формы1
Понятные тексты ошибок (называют правило и пример), чистый код1
Подсказка по времени

Закладывайте 2–3 часа. Сначала сделайте только валидаторы и проверьте их в консоли на 5–10 значениях, потом подключайте страницу. Если застряли — сначала 15 минут сами, потом вопрос в чат группы (с кодом и текстом ошибки), потом ИИ по формуле промпта.

✓ Шпаргалка урока

  • Читаем поля: form.elements.name.value для текста, числа, списка и группы радио; .checked для чекбокса. value — всегда строка, число — через Number(...) или valueAsNumber.
  • submit: слушаем на форме, первой строкой event.preventDefault(), значения читаем внутри обработчика. form.reset() — очистить, form.dispatchEvent(new Event("submit", { cancelable: true })) — отправить программно.
  • FormData: new FormData(form), .get / .getAll / .has, Object.fromEntries(data). Без name, с disabled и неотмеченные чекбоксы в данные не попадают.
  • Встроенная валидация: required, type, min/max, pattern; checkValidity(), validity.*, setCustomValidity(). Свои тексты и сложные правила — своим JS и novalidate.
  • Регулярки: /шаблон/.test(строка). \d цифра, \s пробел, [a-z] диапазон, + * ? {n,m} количество, ^ $ якоря начала и конца. Телефон: /^\+992\d{9}$/. Всегда тестируйте на плохих данных.
  • Свой валидатор — функция «значение → текст ошибки или ""». Все поля — в объекте validators, ошибки показываем через textContent под полем, плюс aria-invalid. Тексты ошибок называют правило и пример.
  • Живая проверка: blur делает поле «тронутым», после этого input перепроверяет на каждый ввод. Сравнение двух полей — слушаем оба. Проверка при submit остаётся обязательной.
  • ИИ: просим правила валидации по формуле промпта и всегда проверяем регулярки на своих кейсах: подходящие, пустая строка, границы, мусор вокруг, реальные данные. Не вставляем код, который не можем объяснить. Проверка в браузере — удобство, защита — на сервере.