Создаём многоступенчатые формы с помощью JavaScript и CSS
Многоэтапные формы — хороший выбор, если ваша форма большая и содержит много элементов управления. Никому не хочется прокручивать очень длинную форму на мобильном устройстве. Группируя элементы управления на каждом экране, мы можем упростить заполнение длинных и сложных форм.
Но когда вы в последний раз разрабатывали многоэтапную форму? Вам это вообще кажется забавным? Нужно столько всего продумать и столько всего учесть, что я не буду винить вас, если вы воспользуетесь библиотекой форм или даже каким-нибудь виджетом для форм, который сделает всё за вас.
Но выполнение работы вручную может стать хорошим упражнением и отличным способом отточить базовые навыки. Я покажу вам, как я создал свою первую многоэтапную форму, и надеюсь, что вы не только увидите, насколько она проста в использовании, но и, возможно, даже заметите, что можно улучшить.
Мы вместе разберём структуру. Мы создадим приложение для поиска работы, с которым, я думаю, многие из нас сталкивались в последние дни. Сначала я создам базовую структуру HTML, CSS и JavaScript, а затем мы рассмотрим вопросы доступности и проверки.
Структура многоступенчатой формы
Наша форма заявки на вакансию состоит из четырёх разделов, последний из которых — это сводная таблица, в которой мы показываем пользователю все его ответы перед отправкой. Для этого мы делим HTML-код на четыре раздела, каждый из которых имеет идентификатор, и добавляем навигацию в нижней части страницы. Я приведу базовый HTML-код в следующем разделе.
Чтобы пользователь мог перемещаться по разделам, мы также добавим визуальный индикатор, показывающий, на каком этапе он находится и сколько шагов осталось. Этот индикатор может быть простым динамическим текстом, который обновляется в соответствии с активным этапом, или более сложным индикатором в виде шкалы прогресса. Мы выберем первый вариант, чтобы упростить процесс и сосредоточиться на многоэтапном характере формы.
Структура и основные стили
Мы сосредоточимся на логике, но в конце я приведу фрагменты кода и ссылку на полный код.
Давайте начнём с создания папки для хранения наших страниц. Затем создайте файл index.html и вставьте в него следующее:
<form id="myForm">
<section class="group-one" id="one">
<div class="form-group">
<div class="form-control">
<label for="name">Name <span style="color: red;">*</span></label>
<input type="text" id="name" name="name" placeholder="Enter your name">
</div>
<div class="form-control">
<label for="idNum">ID number <span style="color: red;">*</span></label>
<input type="number" id="idNum" name="idNum" placeholder="Enter your ID number">
</div>
</div>
<div class="form-group">
<div class="form-control">
<label for="email">Email <span style="color: red;">*</span></label>
<input type="email" id="email" name="email" placeholder="Enter your email">
</div>
<div class="form-control">
<label for="birthdate">Date of Birth <span style="color: red;">*</span></label>
<input type="date" id="birthdate" name="birthdate" max="2006-10-01" min="1924-01-01">
</div>
</div>
</section>
<section class="group-two" id="two">
<div class="form-control">
<label for="document">Upload CV <span style="color: red;">*</span></label>
<input type="file" name="document" id="document">
</div>
<div class="form-control">
<label for="department">Department <span style="color: red;">*</span></label>
<select id="department" name="department">
<option value="">Select a department</option>
<option value="hr">Human Resources</option>
<option value="it">Information Technology</option>
<option value="finance">Finance</option>
</select>
</div>
</section>
<section class="group-three" id="three">
<div class="form-control">
<label for="skills">Skills (Optional)</label>
<textarea id="skills" name="skills" rows="4" placeholder="Enter your skills"></textarea>
</div>
<div class="form-control">
<input type="checkbox" name="terms" id="terms">
<label for="terms">I agree to the terms and conditions <span style="color: red;">*</span></label>
</div>
<button id="btn" type="submit">Confirm and Submit</button>
</section>
<div class="arrows">
<button type="button" id="navLeft">Previous</button>
<span id="stepInfo"></span>
<button type="button" id="navRight">Next</button>
</div>
</form>
<script src="script.js"></script>
Если посмотреть на код, можно увидеть три раздела и группу навигации. Разделы содержат поля формы и не имеют встроенной проверки формы. Это позволяет нам лучше контролировать отображение сообщений об ошибках, поскольку встроенная проверка формы запускается только при нажатии кнопки отправки.
Затем создайте styles.css файл и вставьте в него это:
:root {
--primary-color: #8c852a;
--secondary-color: #858034;
}
body {
font-family: sans-serif;
line-height: 1.4;
margin: 0 auto;
padding: 20px;
background-color: #f4f4f4;
max-width: 600px;
}
h1 {
text-align: center;
}
form {
background: #fff;
padding: 40px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.form-group {
display: flex;
gap: 7%;
}
.form-group > div {
width: 100%;
}
input:not([type="checkbox"]),
select,
textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-control {
margin-bottom: 15px;
}
button {
display: block;
width: 100%;
padding: 10px;
color: white;
background-color: var(--primary-color);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: var(--secondary-color);
}
.group-two, .group-three {
display: none;
}
.arrows {
display: flex;
justify-content: space-between
align-items: center;
margin-top: 10px;
}
#navLeft, #navRight {
width: fit-content;
}
@media screen and (max-width: 600px) {
.form-group {
flex-direction: column;
}
}
Откройте HTML-файл в браузере, и вы увидите что-то вроде двухколоночного макета на следующем скриншоте с индикатором текущей страницы и навигацией.
Добавление функциональности с помощью ванильного JavaScript
Теперь создайте файл script.js в том же каталоге, что и файлы HTML и CSS, и вставьте в него следующий код JavaScript:
const stepInfo = document.getElementById("stepInfo");
const navLeft = document.getElementById("navLeft");
const navRight = document.getElementById("navRight");
const form = document.getElementById("myForm");
const formSteps = ["one", "two", "three"];
let currentStep = 0;
function updateStepVisibility() {
formSteps.forEach((step) => {
document.getElementById(step).style.display = "none";
});
document.getElementById(formSteps[currentStep]).style.display = "block";
stepInfo.textContent = `Step ${currentStep + 1} of ${formSteps.length}`;
navLeft.style.display = currentStep === 0 ? "none" : "block";
navRight.style.display =
currentStep === formSteps.length - 1 ? "none" : "block";
}
document.addEventListener("DOMContentLoaded", () => {
navLeft.style.display = "none";
updateStepVisibility();
navRight.addEventListener("click", () => {
if (currentStep < formSteps.length - 1) {
currentStep++;
updateStepVisibility();
}
});
navLeft.addEventListener("click", () => {
if (currentStep > 0) {
currentStep--;
updateStepVisibility();
}
});
});
Этот скрипт определяет метод, который показывает и скрывает раздел в зависимости от значений formStep , соответствующих идентификаторам разделов формы. Он обновляет stepInfo в соответствии с текущим активным разделом формы. Этот динамический текст служит индикатором прогресса для пользователя.
Затем он добавляет логику, которая ожидает загрузки страницы и реагирует на нажатия кнопок навигации, чтобы можно было переключаться между различными разделами формы. Если вы обновите страницу, то увидите, что многоступенчатая форма работает должным образом.
Многоступенчатая навигация по форме
Давайте подробнее рассмотрим, что делает приведенный выше код Javascript. В функции updateStepVisibility() мы сначала скрываем все разделы, чтобы начать с чистого листа:
formSteps.forEach((step) => {
document.getElementById(step).style.display = "none";
});
Затем мы показываем текущий активный раздел:
document.getElementById(formSteps[currentStep]).style.display = "block";`
Далее мы обновляем текст, который показывает прогресс прохождения формы:
stepInfo.textContent = `Step ${currentStep + 1} of ${formSteps.length}`;
Наконец, мы скрываем кнопку «Предыдущий шаг», если находимся на первом шаге, и скрываем кнопку «Следующий шаг», если находимся в последнем разделе:
navLeft.style.display = currentStep === 0 ? "none" : "block";
navRight.style.display = currentStep === formSteps.length - 1 ? "none" : "block";
Давайте посмотрим, что происходит при загрузке страницы. Сначала мы скрываем кнопку «Назад» (Previous), когда форма загружается в первом разделе:
document.addEventListener("DOMContentLoaded", () => {
navLeft.style.display = "none";
updateStepVisibility();
Затем мы берём кнопку «Далее» (Next) и добавляем событие щелчка, которое условно увеличивает текущий счётчик шагов, а затем вызывает функцию updateStepVisibility(), которая обновляет отображаемый раздел:
navRight.addEventListener("click", () => {
if (currentStep < formSteps.length - 1) {
currentStep++;
updateStepVisibility();
}
});
Наконец, мы нажимаем кнопку «Назад» (Previous) и делаем то же самое, но в обратном порядке. Здесь мы условно уменьшаем количество шагов и вызываем updateStepVisibility():
navLeft.addEventListener("click", () => {
if (currentStep > 0) {
currentStep--;
updateStepVisibility();
}
});
Обработка ошибок
Вы когда-нибудь тратили добрых 10 минут на заполнение формы только для того, чтобы отправить её и получить расплывчатые сообщения об ошибках с рекомендациями исправить то-то и то-то? Я предпочитаю, чтобы форма сразу сообщала мне, что что-то не так, чтобы я мог исправить это до того, как нажму кнопку «Отправить» (Submit). Именно это мы и сделаем в нашей форме.
Наш принцип заключается в том, чтобы чётко указывать, в каких элементах управления есть ошибки, и выдавать понятные сообщения об ошибках. Устраняйте ошибки по мере того, как пользователь выполняет необходимые действия. Давайте добавим проверку в нашу форму. Сначала давайте возьмём необходимые элементы ввода и добавим их к существующим:
const nameInput = document.getElementById("name");
const idNumInput = document.getElementById("idNum");
const emailInput = document.getElementById("email");
const birthdateInput = document.getElementById("birthdate")
const documentInput = document.getElementById("document");
const departmentInput = document.getElementById("department");
const termsCheckbox = document.getElementById("terms");
const skillsInput = document.getElementById("skills");
Затем добавьте функцию для проверки выполненных шагов:
function validateStep(step) {
let isValid = true;
if (step === 0) {
if (nameInput.value.trim() === "")
showError(nameInput, "Name is required");
isValid = false;
}
if (idNumInput.value.trim() === "") {
showError(idNumInput, "ID number is required");
isValid = false;
}
if (emailInput.value.trim() === "" || !emailInput.validity.valid) {
showError(emailInput, "A valid email is required");
isValid = false;
}
if (birthdateInput.value === "") {
showError(birthdateInput, "Date of birth is required");
isValid = false;
}
else if (step === 1) {
if (!documentInput.files[0]) {
showError(documentInput, "CV is required");
isValid = false;
}
if (departmentInput.value === "") {
showError(departmentInput, "Department selection is required");
isValid = false;
}
} else if (step === 2) {
if (!termsCheckbox.checked) {
showError(termsCheckbox, "You must accept the terms and conditions");
isValid = false;
}
}
return isValid;
}
Здесь мы проверяем, есть ли у каждого обязательного поля ввода какое-либо значение и есть ли у поля ввода электронной почты допустимый ввод. Затем мы соответствующим образом устанавливаем логическое значение isValid. Мы также вызываем функцию showError(), которую ещё не определили.
Вставьте этот код над функцией validateStep():
function showError(input, message) {
const formControl = input.parentElement;
const errorSpan = formControl.querySelector(".error-message");
input.classList.add("error");
errorSpan.textContent = message;
}
Теперь добавьте в таблицу стилей следующие стили:
input:focus, select:focus, textarea:focus {
outline: .5px solid var(--primary-color);
}
input.error, select.error, textarea.error {
outline: .5px solid red;
}
.error-message {
font-size: x-small;
color: red;
display: block;
margin-top: 2px;
}
.arrows {
color: var(--primary-color);
font-size: 18px;
font-weight: 900;
}
#navLeft, #navRight {
display: flex;
align-items: center;
gap: 10px;
}
#stepInfo {
color: var(--primary-color);
}
Если вы обновите форму, то увидите, что кнопки не переведут вас в следующий раздел, пока поля не будут заполнены правильно:
Наконец, мы хотим добавить обработку ошибок в реальном времени, чтобы ошибки исчезали, когда пользователь начинает вводить правильную информацию. Добавьте эту функцию под функцию validateStep():
function setupRealtimeValidation() {
nameInput.addEventListener("input", () => {
if (nameInput.value.trim() !== "") clearError(nameInput);
});
idNumInput.addEventListener("input", () => {
if (idNumInput.value.trim() !== "") clearError(idNumInput);
});
emailInput.addEventListener("input", () => {
if (emailInput.validity.valid) clearError(emailInput);
});
birthdateInput.addEventListener("change", () => {
if (birthdateInput.value !== "") clearError(birthdateInput);
});
documentInput.addEventListener("change", () => {
if (documentInput.files[0]) clearError(documentInput);
});
departmentInput.addEventListener("change", () => {
if (departmentInput.value !== "") clearError(departmentInput);
});
termsCheckbox.addEventListener("change", () => {
if (termsCheckbox.checked) clearError(termsCheckbox);
});
}
Эта функция очищает ошибки, если ввод больше не является некорректным, прослушивая события ввода и изменения, а затем вызывая функцию для очистки ошибок. Вставьте функцию clearError() под функцию
showError():
function clearError(input) {
const formControl = input.parentElement;
const errorSpan = formControl.querySelector(".error-message");
input.classList.remove("error");
errorSpan.textContent = "";
}
И теперь ошибки устраняются, когда пользователь вводит правильное значение:
Многоступенчатая форма теперь корректно обрабатывает ошибки. Если вы решите оставить ошибки до конца формы, то, по крайней мере, верните пользователя к элементу формы с ошибкой и покажите, сколько ошибок ему нужно исправить.
Обработка отправки формы
В многошаговой форме полезно показывать пользователю сводку всех его ответов в конце, прежде чем он отправит форму, и предлагать ему возможность отредактировать ответы, если это необходимо. Пользователь не может увидеть предыдущие шаги, не вернувшись назад, поэтому сводка на последнем шаге даёт уверенность и возможность исправить ошибки.
Давайте добавим в разметку четвёртый раздел для отображения этой сводной информации и переместим в него кнопку отправки. Вставьте это чуть ниже третьего раздела в index.html:
<section class="group-four" id="four">
<div class="summary-section">
<p>Name: </p>
<p id="name-val"></p>
<button type="button" class="edit-btn" id="name-edit">
<span>✎</span>
<span>Edit</span>
</button>
</div>
<div class="summary-section">
<p>ID Number: </p>
<p id="id-val"></p>
<button type="button" class="edit-btn" id="id-edit">
<span>✎</span>
<span>Edit</span>
</button>
</div>
<div class="summary-section">
<p>Email: </p>
<p id="email-val"></p>
<button type="button" class="edit-btn" id="email-edit">
<span>✎</span>
<span>Edit</span>
</button>
</div>
<div class="summary-section">
<p>Date of Birth: </p>
<p id="bd-val"></p>
<button type="button" class="edit-btn" id="bd-edit">
<span>✎</span>
<span>Edit</span>
</button>
</div>
<div class="summary-section">
<p>CV/Resume: </p>
<p id="cv-val"></p>
<button type="button" class="edit-btn" id="cv-edit">
<span>✎</span>
<span>Edit</span>
</button>
</div>
<div class="summary-section">
<p>Department: </p>
<p id="dept-val"></p>
<button type="button" class="edit-btn" id="dept-edit">
<span>✎</span>
<span>Edit</span>
</button>
</div>
<div class="summary-section">
<p>Skills: </p>
<p id="skills-val"></p>
<button type="button" class="edit-btn" id="skills-edit">
<span>✎</span>
<span>Edit</span>
</button>
</div>
<button id="btn" type="submit">Confirm and Submit</button>
</section>
Затем обновите formStep в вашем Javascript, чтобы читать:
const formSteps = ["one", "two", "three", "four"];
Наконец, добавьте следующие классы в styles.css:
.summary-section {
display: flex;
align-items: center;
gap: 10px;
}
.summary-section p:first-child {
width: 30%;
flex-shrink: 0;
border-right: 1px solid var(--secondary-color);
}
.summary-section p:nth-child(2) {
width: 45%;
flex-shrink: 0;
padding-left: 10px;
}
.edit-btn {
width: 25%;
margin-left: auto;
background-color: transparent;
color: var(--primary-color);
border: .7px solid var(--primary-color);
border-radius: 5px;
padding: 5px;
}
.edit-btn:hover {
border: 2px solid var(--primary-color);
font-weight: bolder;
background-color: transparent;
}
Теперь добавьте следующее в начало файла script.js там, где находятся другие const:
const nameVal = document.getElementById("name-val");
const idVal = document.getElementById("id-val");
const emailVal = document.getElementById("email-val");
const bdVal = document.getElementById("bd-val")
const cvVal = document.getElementById("cv-val");
const deptVal = document.getElementById("dept-val");
const skillsVal = document.getElementById("skills-val");
const editButtons =
"name-edit": 0,
"id-edit": 0,
"email-edit": 0,
"bd-edit": 0,
"cv-edit": 1,
"dept-edit": 1,
"skills-edit": 2
};
Затем добавьте эту функцию в scripts.js:
function updateSummaryValues() {
nameVal.textContent = nameInput.value;
idVal.textContent = idNumInput.value;
emailVal.textContent = emailInput.value;
bdVal.textContent = birthdateInput.value;
const fileName = documentInput.files[0]?.name;
if (fileName)
const extension = fileName.split(".").pop();
const baseName = fileName.split(".")[0];
const truncatedName = baseName.length > 10 ? baseName.substring(0, 10) + "..." : baseName;
cvVal.textContent = `${truncatedName}.${extension}`;
} else {
cvVal.textContent = "No file selected";
}
deptVal.textContent = departmentInput.value;
skillsVal.textContent = skillsInput.value || "No skills submitted";
}
Это позволяет динамически вставлять значения полей ввода в раздел «Сводка» формы, сокращать названия файлов и предлагать альтернативный текст для полей ввода, которые не были обязательными.
Затем обновите updateStepVisibility() функцию, чтобы вызвать новую функцию:
function updateStepVisibility() {
formSteps.forEach((step) => {
document.getElementById(step).style.display = "none";
});
document.getElementById(formSteps[currentStep]).style.display = "block";
stepInfo.textContent = `Step ${currentStep + 1} of ${formSteps.length}`;
if (currentStep === 3) {
updateSummaryValues();
}
navLeft.style.display = currentStep === 0 ? "none" : "block";
navRight.style.display = currentStep === formSteps.length - 1 ? "none" : "block";
}
Наконец, добавьте это в DOMContentLoaded прослушиватель событий:
Object.keys(editButtons).forEach((buttonId) => {
const button = document.getElementById(buttonId);
button.addEventListener("click", (e) => {
currentStep = editButtons[buttonId];
updateStepVisibility();
});
});
При запуске формы вы увидите, что в разделе «Сводка» отображаются все введённые значения и пользователь может отредактировать их перед отправкой информации:
И теперь мы можем отправить нашу форму:
form.addEventListener("submit", (e) => {
e.preventDefault();
if (validateStep(2)) {
alert("Form submitted successfully!");
form.reset();
currentFormStep = 0;
updateStepVisibility();
}
});
Наша многоступенчатая форма теперь позволяет пользователю редактировать и просматривать всю предоставленную им информацию перед отправкой.
Советы по обеспечению доступности
Чтобы сделать многоэтапные формы доступными, начните с основ: используйте семантический HTML. Это половина дела. Далее следует использовать подходящие метки для форм.
Другие способы сделать формы более доступными включают в себя выделение достаточного места для элементов, на которые нужно нажимать на маленьких экранах, и добавление понятных описаний к навигации по форме и индикаторам выполнения.
Важной частью этого процесса является предоставление пользователю обратной связи. Не стоит автоматически отклонять отзывы пользователей по истечении определённого времени, лучше позволить пользователю отклонить их самостоятельно. Также важно обращать внимание на контрастность и выбор шрифта, поскольку они влияют на читаемость вашей формы.
Давайте внесем следующие коррективы в разметку для большей технической доступности:
- Добавьте aria-required="true" ко всем полям ввода, кроме поля «Навыки». Это позволит программам чтения с экрана понять, что поля обязательны к заполнению, без использования встроенной проверки.
- Добавьте role="alert" в диапазоны ошибок. Это поможет программам чтения с экрана понять, что ввод находится в состоянии ошибки.
- Добавьте role="status" aria-live="polite" в .stepInfo. Это поможет программам чтения с экрана понять, что информация о шаге отслеживает состояние, а значение aria-live, установленное на «вежливый», указывает на то, что при изменении значения не нужно сразу сообщать об этом.
В файле сценария замените функции showError() и clearError() на следующие:
function showError(input, message) {
const formControl = input.parentElement;
const errorSpan = formControl.querySelector(".error-message");
input.classList.add("error");
input.setAttribute("aria-invalid", "true");
input.setAttribute("aria-describedby", errorSpan.id);
errorSpan.textContent = message;
}
function clearError(input) {
const formControl = input.parentElement;
const errorSpan = formControl.querySelector(".error-message");
input.classList.remove("error");
input.removeAttribute("aria-invalid");
input.removeAttribute("aria-describedby");
errorSpan.textContent = "";
}
Здесь мы программно добавляем и удаляем атрибуты, которые явно связывают ввод с диапазоном ошибок и показывают, что он находится в недопустимом состоянии.
Наконец, давайте добавим фокус на первый ввод в каждом разделе. Добавьте следующий код в конец функции updateStepVisibility():
const currentStepElement = document.getElementById(formSteps[currentStep]);
const firstInput = currentStepElement.querySelector(
"input, select, textarea"
);
if (firstInput) {
firstInput.focus();
}
И при этом многоступенчатая форма становится гораздо более доступной.
Заключение
Итак, перед вами многошаговая форма из четырёх частей для подачи заявления о приёме на работу! Как я уже говорил в начале этой статьи, здесь много всего, чем нужно заниматься, — настолько, что я не буду винить вас, если вы будете искать готовое решение.
Но если вам приходится вручную создавать многоэтапную форму, надеюсь, теперь вы понимаете, что это не смертный приговор. Есть простой путь, который приведёт вас к цели, с навигацией и проверкой, без отхода от хороших, доступных практик.
И именно так я к этому подошёл! Опять же, я воспринял это как личный вызов, чтобы посмотреть, как далеко я смогу зайти, и я вполне доволен результатом. Но я был бы рад узнать, видите ли вы дополнительные возможности сделать его ещё более удобным для пользователей и доступным.