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

Зачем форма, если есть почта

Адрес, написанный на странице, за неделю собирают роботы, и дальше на него идёт спам. Форма адрес не показывает: он лежит в настройках сайта, а посетитель просто пишет сообщение.

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

Поле — это не только input

Минимальный набор для одного поля — четыре части: подпись, само поле, при необходимости подсказка и место под сообщение об ошибке. Подпись обязана быть отдельным тегом <label>, связанным с полем: тогда щелчок по подписи ставит курсор в поле, а программа чтения экрана называет поле по имени.

Одно поле целиком
<div class="field">
  <label class="field__label" for="email">Почта</label>

  <input class="field__input" id="email" name="email" type="email"
         required autocomplete="email"
         aria-describedby="email-hint"
         data-msg-required="Без адреса я не смогу ответить"
         data-msg-type="Похоже, в адресе опечатка: нужен знак @">

  <p class="field__hint" id="email-hint">На него придёт ответ</p>
  <p class="field__error" hidden></p>
</div>

Тексты ошибок лежат прямо в атрибутах поля — data-msg-required и остальные. Так они правятся вместе с полем, а не в скрипте: добавили поле — написали рядом, что сказать, если его не заполнят.

Атрибут autocomplete — не мелочь

С ним браузер подставляет имя и почту одним касанием. На телефоне это разница между «отправил за пять секунд» и «набирал адрес пальцем и бросил».

Кто проверяет: браузер, скрипт и сервер

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

Три уровня проверки формы
Кто Что делает Зачем нужен
Браузер Смотрит на required, type, minlength Работает всегда, даже если скрипт не загрузился
Скрипт Показывает понятные подсказки рядом с полем Свои тексты вместо казённых фраз браузера
Сервер Проверяет всё заново перед отправкой письма Первые две проверки обходятся в две минуты

Отсюда важная мелочь: атрибут novalidate, который отключает проверку браузера, ставит скрипт, а не разметка. Пока скрипт не отработал, форму проверяет браузер — и без JavaScript она остаётся рабочей.

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

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

Ещё две вещи делает скрипт при неудачной отправке: связывает сообщение с полем через aria-describedby, ставит aria-invalid и переводит фокус на первое неверное поле. Без последнего человек с программой чтения экрана останется у кнопки и просто не узнает, что где-то выше есть ошибка.

Что попросить у нейросети

Мой запрос

Напиши скрипт проверки формы обратной связи на чистом JavaScript. Поля: имя, почта, сообщение, чекбокс согласия. В разметке уже стоят настоящие условия — required, type="email", minlength, а тексты ошибок лежат в атрибутах поля вида data-msg-required.

Правила: атрибут novalidate форме ставит сам скрипт, чтобы без JavaScript работала проверка браузера. До первой попытки отправить подсказки не показывать, после неё перепроверять поле при вводе и потере фокуса. Ошибку связать с полем через aria-describedby, не затирая уже указанные там подсказки, и поставить aria-invalid.

После неудачной отправки перевести фокус на первое неверное поле. Блок с результатом сделать role="status", причём текст в него писать следующим кадром, уже после снятия hidden. Комментарии по-русски.

Ответ нейросети

Про role="status". Ваше требование писать текст следующим кадром верное, и причина такая: изменение внутри скрытого блока программы чтения экрана не объявляют. Снимаете hidden, затем в requestAnimationFrame кладёте текст — тогда сообщение прозвучит.

Что взять у браузера даром. У каждого поля есть объект validity: он сам говорит, пусто ли поле (valueMissing), не подходит ли формат (typeMismatch), коротко ли значение (tooShort). Свои правила писать не нужно — достаточно сопоставить эти признаки вашим атрибутам с текстами.

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

Отправка письма в Evolution CMS

Вся разметка выше писем не отправляет: HTML умеет только собрать данные и передать их туда, где работает программа на сервере. В Evolution такая программа — сниппет. В поставке 3.5.8 его нет, и отсюда развилка: поставить готовый или написать свой.

Готовый: поставить из каталога

В админке есть модуль Extras — каталог расширений на 171 пакет: нашли, нажали «Установить», настроили параметрами и чанками. С формами там вот что: eForm (22 тысячи установок), FormLister и easyForm помечены «устаревшие» — их писали под прошлые ветки движка. Из свежего есть sMailer, но это не форма, а служба отправки писем: принять поля и проверить их всё равно придётся самому. Так что выбор не «готовое против своего», а «устаревшее готовое против ста своих строк».

Свой: сто строк и метод ядра

Этот сайт пошёл вторым путём: всё сложное уже есть в ядре. Метод getMail() отдаёт готовый почтовый объект, настроенный по разделу «Почта» в конфигурации сайта, — сам соединяется с сервером и кодирует русские буквы в теме. Остаётся проверить поля и попросить его отправить.

Вызов на странице контактов

Нужен только для ответа. Никаких рассылок.

Чанк chunk-contact-form — та же вёрстка формы, что в прототипе, плюс места для введённых значений, сообщений у полей и строки результата. Адрес получателя берётся из настройки сайта emailsender: в разметку страницы он не попадает, и роботы его не увидят.

Скобки тут другие

Форма обязана считаться заново на каждый заход, поэтому вызов некэшируемый — . Запись [[!Сниппет]], привычная по другим движкам, в Evolution означает сниппет с именем «!Сниппет»: он не находится, и на месте формы выходит пустое место без единого сообщения об ошибке.

Что обязан делать обработчик

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

Шесть обязанностей серверной части формы
Что сделано Что случится, если не сделать
Поля проверяются заново Проверка в браузере обходится за две минуты: JavaScript отключается, письмо приходит пустым
Получатель — только из параметра вызова Если брать адрес из скрытого поля формы, любой подставит свой и разошлёт спам с вашего сервера
Из темы и имени вырезаны переносы строк Через перенос в письмо дописывают свои заголовки и добавляют получателей
Письмо уходит от адреса сайта, посетитель — в «ответить» Письмо от чужого адреса почтовый сервер сочтёт подделкой и не пропустит
Значения возвращаются в поля экранированными Иначе отправитель впишет в поле тег и выполнит свой скрипт на вашей странице
Поле-ловушка и пауза между письмами Робот заполняет всё подряд и шлёт без остановки; ловушка отсекает первое, пауза — второе

Частые ошибки

Чем чаще всего ломают форму
Ошибка Чем плохо Как правильно
Подпись — плейсхолдер внутри поля Пропадает при вводе, и человек забывает, что тут было Отдельный label, связанный с полем
novalidate стоит в разметке Без JavaScript форма не проверяется вообще Атрибут ставит скрипт при запуске
Ошибка показана только цветом рамки Человек не видит цвета или не понимает, что не так Текст рядом с полем плюс aria-invalid
Проверка только на странице Обходится отключением JavaScript за две минуты Обязательная проверка на сервере
«Письмо отправлено», когда его нет Человек ждёт ответа, которого никто не получал Сообщать правду о том, что произошло

Что должно получиться

  • Щелчок по подписи ставит курсор в поле
  • До первой отправки форма не ругается, после — подсказки исчезают по мере исправления
  • Фокус после неудачной отправки стоит на первом неверном поле
  • С выключенным JavaScript форму по-прежнему проверяет браузер
  • Почтовый адрес получателя нигде не выведен на страницу