JS: Form with steps and validation
JS: Form with steps and validation
Glossary overview

JS: Form with steps and validation

HTML:

<form class="add-post" method="post" action="<?php echo admin_url('admin-post.php'); ?>" enctype="multipart/form-data">
                <div class="form-steps" id="formSteps">
                    <button type="button" class="step is-active" data-step="0">
                        <span class="step-num">1</span><span class="step-title">Основное</span>
                    </button>
                    <button type="button" class="step" data-step="1">
                        <span class="step-num">2</span><span class="step-title">Параметры</span>
                    </button>
                    <button type="button" class="step" data-step="2">
                        <span class="step-num">3</span><span class="step-title">Цены</span>
                    </button>
                    <button type="button" class="step" data-step="3">
                        <span class="step-num">4</span><span class="step-title">Услуги</span>
                    </button>
                    <button type="button" class="step" data-step="4">
                        <span class="step-num">5</span><span class="step-title">Фото</span>
                    </button>
                    <button type="button" class="step" data-step="5">
                        <span class="step-num">6</span><span class="step-title">Размещение</span>
                    </button>
                </div>

                <section class="tab is-active" data-tab="0">
                    <label for="name">Имя <span class="required">*</span></label>
                    <input type="text" id="name" name="name" required minlength="1" maxlength="20" size="10" />

                    <label for="description">Описание <span class="required">*</span></label>
                    <textarea name="description" id="description" cols="30" rows="10" required></textarea>

                    <label for="phone">Номер телефона <span class="required">*</span></label>
                    <input type="text" id="phone" name="phone" required minlength="17" maxlength="17" size="10" />

                    <label for="whatsapp">WhatsApp</label>
                    <input type="text" id="whatsapp" name="whatsapp" minlength="11" maxlength="11" size="10" />

                    <label for="viber">Viber</label>
                    <input type="text" id="viber" name="viber" minlength="11" maxlength="11" size="10" />

                    <label for="telegram">Telegram ник</label>
                    <input type="text" id="telegram" name="telegram" minlength="3" maxlength="30" size="10" />

                    <label for="metro">Станция Метро <span class="required">*</span></label>
                    <select name="metro" id="metro" required>
                        <?php
                        $terms = get_terms([
                                'taxonomy' => 'metro',
                                'hide_empty' => false,
                                'orderby' => 'name',
                                'order' => 'ASC',
                        ]);

                        if (! is_wp_error($terms)) {
                            foreach ($terms as $term) {
                                printf(
                                        '<option value="%1$s">%2$s</option>',
                                        $term->term_id,
                                        esc_html($term->name),
                                );
                            }
                        }
                        ?>
                    </select>

                    <label for="area">Район <span class="required">*</span></label>
                    <select name="area" id="area" required>
                        <?php
                        $terms = get_terms([
                                'taxonomy' => 'area',
                                'hide_empty' => false,
                                'orderby' => 'name',
                                'order' => 'ASC',
                        ]);

                        if (! is_wp_error($terms)) {
                            foreach ($terms as $term) {
                                printf(
                                        '<option value="%1$s">%2$s</option>',
                                        $term->term_id,
                                        esc_html($term->name),
                                );
                            }
                        }
                        ?>
                    </select>
                </section>

                <section class="tab" data-tab="1">
                   ...
                </section>
                 <section class="tab" data-tab="2">
                   ...
                </section>
                 ....

                <input type="hidden" name="action" value="add_post">
                <?php wp_nonce_field('add_post_nonce'); ?>

                <div class="tab-actions">
                    <button type="button" class="btn-prev" id="btnPrev">Назад</button>

                    <button type="button" class="btn-next" id="btnNext">Следующий шаг</button>

                    <input type="submit" class="btn-submit" id="btnSubmit" name="submit" value="Опубликовать">
                </div>

 </form>

Внешний вид.

Первый шаг.

Второй шаг.

Последний шаг.

Валидация.

При переходе на следующий шаг, мы валидируем поля на текущем шаге, а не всю форму.

JavaScript

(function ($) {
    $(document).ready(function () {

        const $form = $('form.add-post');
        const $tabs = $('.tab');
        const $steps = $('#formSteps .step');

        const $btnPrev = $('#btnPrev');
        const $btnNext = $('#btnNext');
        const $btnSubmit = $('#btnSubmit');

        let current = 0;

        function setActive(index) {
            current = index;

            $tabs.removeClass('is-active').eq(index).addClass('is-active');

            $steps.each(function (i) {
                $(this)
                    .toggleClass('is-active', i === index)
                    .toggleClass('is-done', i < index);
            });

            $btnPrev.toggle(index !== 0);

            const isLast = index === ($tabs.length - 1);
            $btnNext.toggle(!isLast);
            $btnSubmit.toggle(isLast);

            const stepsEl = document.getElementById('formSteps');
            if (stepsEl) stepsEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }

        function validateCurrentTab() {
            const tabEl = $tabs.get(current);
            if (!tabEl) return true;

            const fields = tabEl.querySelectorAll('input, select, textarea');

            for (const el of fields) {
                if (el.disabled) continue;

                if (!el.checkValidity()) {
                    el.reportValidity();
                    return false;
                }
            }
            return true;
        }

        $btnNext.on('click', function () {
            if (!validateCurrentTab()) return;
            setActive(Math.min(current + 1, $tabs.length - 1));
        });

        $btnPrev.on('click', function () {
            setActive(Math.max(current - 1, 0));
        });

        $steps.on('click', function () {
            const target = Number($(this).data('step'));
            if (Number.isNaN(target) || target === current) return;

            if (target > current) {
                if (!validateCurrentTab()) return;
                if (target !== current + 1) return;
            }

            setActive(target);
        });

        $form.on('submit', function (e) {
            const isLast = current === ($tabs.length - 1);

            if (!isLast) {
                e.preventDefault();
                if (validateCurrentTab()) setActive(current + 1);
                return;
            }

            if (!validateCurrentTab()) {
                e.preventDefault();
            }
        });

        // start
        setActive(0);

    });
})(jQuery);

Объяснение

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

1) Как переключаются табы

Вся смена шага идет через одну функцию setActive(index).

Что она делает:

  1. Показывает только нужный tab
$tabs.removeClass('is-active').eq(index).addClass('is-active');

2. Синхронизирует верхние “шаги”

.toggleClass('is-active', i === index) // текущий шаг
.toggleClass('is-done', i < index)     // уже пройденные

То есть шаги просто получают классы, а внешний вид рисуется CSS-ом.

  1. Переключает кнопки внизу
  • Назад скрывается на первом табе
  • Следующий шаг показывается на всех, кроме последнего
  • Опубликовать показывается только на последнем

Это делается через:

const isLast = index === ($tabs.length - 1);
$btnNext.toggle(!isLast);
$btnSubmit.toggle(isLast);

4. Подскролливает к шапке шагов, чтобы при переходе на следующий таб пользователь не оказывался “в середине” формы.

2) Как валидируется только часть формы (текущий таб)

Ключевой момент: validateCurrentTab() берет только текущую секцию:

const tabEl = $tabs.get(current);
const fields = tabEl.querySelectorAll('input, select, textarea');

И дальше проверяет только эти поля:

  • disabled пропускаются (важно, потому что у тебя куча полей включается/выключается чекбоксами)
  • checkValidity() прогоняет стандартные HTML-правила (required, minlength, pattern и т.д.)
  • reportValidity() показывает браузерное сообщение на первом проблемном поле и сразу останавливает проверку.

checkValidity() и reportValidity() это встроенные методы DOM для элементов формы (HTML5 constraint validation).

  • el.checkValidity()
    Проверяет поле по его ограничениям (required, minlength, maxlength, min/max, pattern, type="email" и т.д.).
    Возвращает true/false. Ничего не показывает пользователю.
  • el.reportValidity()
    Делает то же самое, но если поле невалидно, браузер показывает стандартную подсказку/ошибку и фокусируется на поле.
    Возвращает true/false

То есть валидация “поштучно” и только внутри активного таба.

Почему это вообще работает с required в других табах:

  • потому что при клике Next ты не отправляешь форму, а просто валидируешь текущий таб и переключаешься
  • а при submit на не-последнем шаге submit перехватывается (см. ниже)
  • плюс обычно на форме ставят novalidate, чтобы браузер не пытался валидировать всё сразу при submit (Я не ставил).

3) Как контролируется отправка формы

Тут два сценария.

A) Нажали “Следующий шаг”

Это обычная кнопка type="button", она вообще не отправляет форму.
Код:

$btnNext.on('click', function () {
  if (!validateCurrentTab()) return;
  setActive(current + 1);
});

То есть “Next” = “проверь текущий таб → если ок, покажи следующий”.

B) Нажали “Опубликовать”

Это type="submit", браузер пытается отправить форму, и срабатывает обработчик:

$form.on('submit', function (e) {
  const isLast = current === ($tabs.length - 1);

  if (!isLast) {
    e.preventDefault();
    if (validateCurrentTab()) setActive(current + 1);
    return;
  }

  if (!validateCurrentTab()) {
    e.preventDefault();
  }
});

Логика такая:

  • Если submit случился НЕ на последнем шаге:
    • preventDefault() блокирует отправку
    • форма ведет себя как “Next”: валидирует текущий таб и перекидывает дальше
    • это защита от случаев типа “пользователь нажал Enter в поле” или если где-то случайно появится submit-кнопка
  • Если submit на последнем шаге:
    • валидируется последний таб
    • если он валиден, preventDefault() не вызывается и форма реально уходит на admin-post.php

Итого: реально форма отправляется только когда current = последний таб и текущий таб проходит проверку.