Клиент зашёл в ваш интернет-магазин, хочет найти синие кроссовки размера 43 стоимостью до 5000 рублей. Он тыкает в фильтр — и страница перезагружается. Ждёт. Секунда, вторая, пятая… Закрывает вкладку. Уходит к конкуренту, где фильтрация работает мгновенно, без перезагрузки.

Знакомая ситуация? Именно поэтому AJAX-фильтр для каталога WooCommerce — это не роскошь, а необходимость. По данным Baymard Institute, 42% крупных интернет-магазинов реализуют фильтрацию товаров через AJAX. И знаете что — у них конверсия выше.

Сегодня разберём, как сделать такой фильтр с нуля. Без тяжёлых плагинов. Чистый PHP + JavaScript. Поехали.

Зачем делать AJAX-фильтр для WooCommerce вручную

«Стойте, — скажете вы, — есть же готовые плагины. Filter Everything, WOOF, JetSmartFilters…» Да, есть. И я сам их использовал на десятках проектов. Но у каждого свои нюансы:

  • WOOF — мощный, но тяжёлый. На крупных каталогах (5000+ товаров) может серьёзно тормозить
  • Filter Everything — неплохой, но бесплатная версия ограничена
  • JetSmartFilters — работает только с Elementor и JetEngine

А иногда у клиента специфические требования: нестандартная логика фильтрации, кастомные поля, сложные зависимости между параметрами. И вот тут готовый плагин превращается из решения в проблему. Приходится городить хаки, переписывать шаблоны, страдать при обновлениях.

Свой AJAX-фильтр — это полный контроль. Никаких лишних запросов к базе, никакого мусорного кода. Только то, что нужно вашему проекту.

Архитектура: из чего состоит AJAX-фильтр

Прежде чем писать код, давайте поймём структуру. Любой AJAX-фильтр для каталога WooCommerce состоит из четырёх частей:

  • Фронтенд (HTML) — форма фильтра с чекбоксами, радиокнопками, слайдерами
  • JavaScript — перехватывает изменения в форме и отправляет запрос на сервер
  • PHP-обработчик — принимает параметры, формирует WP_Query и возвращает результат
  • Шаблон вывода — HTML-разметка карточек товаров для обновления на странице

Всё. Никакой магии. Давайте реализовывать каждый элемент.

Шаг 1: HTML-разметка формы фильтра

Создадим шорткод, чтобы вывести фильтр в любом месте сайта. Открываем functions.php (или лучше — файл в плагине-модуле, но об этом позже):

function wenesis_ajax_filter_shortcode() {
    // Получаем все значения атрибута "Цвет"
    $colors = wc_get_attribute_terms(1, 'pa_color'); // ID атрибута = 1
    
    // Получаем категории товаров
    $categories = get_terms(array(
        'taxonomy' => 'product_cat',
        'hide_empty' => true,
        'parent'   => 0 // только родительские
    ));
    
    ob_start();
    ?>
    <form id="ajax-product-filter" class="wc-filter-form">
        
        <div class="filter-section">
            <h4>Категория</h4>
            <ul class="filter-list">
                <?php foreach ($categories as $cat) : ?>
                <li>
                    <label>
                        <input type="checkbox" 
                               name="category&#91;&#93;" 
                               value="<?php echo esc_attr($cat->slug); ?>">
                        <?php echo esc_html($cat->name); ?>
                        <span class="count">(<?php echo $cat->count; ?>)</span>
                    </label>
                </li>
                <?php endforeach; ?>
            </ul>
        </div>
        
        <div class="filter-section">
            <h4>Цвет</h4>
            <ul class="filter-list">
                <?php foreach ($colors as $color) : ?>
                <li>
                    <label>
                        <input type="checkbox" 
                               name="color&#91;&#93;" 
                               value="<?php echo esc_attr($color->slug); ?>">
                        <?php echo esc_html($color->name); ?>
                    </label>
                </li>
                <?php endforeach; ?>
            </ul>
        </div>
        
        <div class="filter-section">
            <h4>Цена</h4>
            <div class="price-range">
                <input type="number" name="min_price" placeholder="От" min="0">
                <span>—</span>
                <input type="number" name="max_price" placeholder="До" min="0">
            </div>
        </div>
        
        <div class="filter-section">
            <h4>Сортировка</h4>
            <select name="orderby">
                <option value="date">Новинки</option>
                <option value="price">Цена: по возрастанию</option>
                <option value="price-desc">Цена: по убыванию</option>
                <option value="popularity">Популярность</option>
                <option value="rating">Рейтинг</option>
            </select>
        </div>
        
        <button type="submit" class="filter-submit">Применить</button>
        <button type="reset" class="filter-reset">Сбросить</button>
    </form>
    <?php
    return ob_get_clean();
}
add_shortcode('ajax_product_filter', 'wenesis_ajax_filter_shortcode');
&#91;/php&#93;

<p>Обратите внимание: я использую <code>ob_start()</code> и <code>ob_get_clean()</code> — это стандартный паттерн для шорткодов в WordPress. Не <code>echo</code>, а именно <code>return</code>.</p>

<h2>Шаг 2: JavaScript — отправка AJAX-запроса</h2>

<p>Теперь создадим скрипт, который будет перехватывать отправку формы и обновлять товары без перезагрузки. Подключаем его правильно — через <code>wp_enqueue_script</code>:</p>

[php]
function wenesis_enqueue_filter_assets() {
    if (!is_shop() && !is_product_category()) return;
    
    wp_enqueue_script(
        'wenesis-filter',
        get_stylesheet_directory_uri() . '/js/ajax-filter.js',
        array('jquery'),
        '1.0',
        true
    );
    
    wp_localize_script('wenesis-filter', 'wenesisFilter', array(
        'ajaxurl' => admin_url('admin-ajax.php'),
        'nonce'   => wp_create_nonce('wenesis_filter_nonce')
    ));
}
add_action('wp_enqueue_scripts', 'wenesis_enqueue_filter_assets');

А вот и сам JavaScript. Ключевой момент — использование делегирования событий через .on():

jQuery(document).ready(function($) {
    
    let filterForm = $('#ajax-product-filter');
    let productList = $('.products'); // контейнер товаров WooCommerce
    let isLoading = false;
    
    // Обработка отправки формы
    filterForm.on('submit', function(e) {
        e.preventDefault();
        applyFilter();
    });
    
    // Сброс фильтра
    filterForm.on('reset', function() {
        setTimeout(function() {
            applyFilter();
        }, 10);
    });
    
    function applyFilter() {
        if (isLoading) return;
        isLoading = true;
        
        // Собираем данные формы
        let formData = filterForm.serialize();
        formData += '&action=filter_products&nonce=' + wenesisFilter.nonce;
        
        // Показываем индикатор загрузки
        productList.addClass('loading');
        
        $.ajax({
            url: wenesisFilter.ajaxurl,
            type: 'POST',
            data: formData,
            success: function(response) {
                if (response.success) {
                    productList.html(response.data.html);
                    
                    // Обновляем счётчик товаров
                    $('.woocommerce-result-count').html(response.data.count_text);
                } else {
                    console.error('Ошибка фильтрации:', response.data);
                }
            },
            error: function(xhr, status, error) {
                console.error('AJAX error:', error);
            },
            complete: function() {
                isLoading = false;
                productList.removeClass('loading');
            }
        });
    }
});

Обратите внимание на проверку isLoading — она предотвращает множественные запросы, если пользователь быстро тыкает в чекбоксы. Мелочь, а на больших каталогах спасает от утечек памяти.

Шаг 3: PHP-обработчик на стороне сервера

Сердце всего механизма. Здесь формируется запрос к базе данных:

function wenesis_filter_products() {
    // Проверяем nonce
    if (!wp_verify_nonce($_POST['nonce'] ?? '', 'wenesis_filter_nonce')) {
        wp_send_json_error('Security check failed');
    }
    
    // Получаем параметры из запроса
    $categories = isset($_POST['category']) ? array_map('sanitize_text_field', $_POST['category']) : array();
    $colors     = isset($_POST['color']) ? array_map('sanitize_text_field', $_POST['color']) : array();
    $min_price  = isset($_POST['min_price']) ? floatval($_POST['min_price']) : '';
    $max_price  = isset($_POST['max_price']) ? floatval($_POST['max_price']) : '';
    $orderby    = isset($_POST['orderby']) ? sanitize_text_field($_POST['orderby']) : 'date';
    $paged      = isset($_POST['page']) ? intval($_POST['page']) : 1;
    
    // Параметры сортировки
    $order_args = array();
    switch ($orderby) {
        case 'price':
            $order_args = array('meta_key' => '_price', 'orderby' => 'meta_value_num', 'order' => 'ASC');
            break;
        case 'price-desc':
            $order_args = array('meta_key' => '_price', 'orderby' => 'meta_value_num', 'order' => 'DESC');
            break;
        case 'popularity':
            $order_args = array('meta_key' => 'total_sales', 'orderby' => 'meta_value_num', 'order' => 'DESC');
            break;
        case 'rating':
            $order_args = array('meta_key' => '_wc_average_rating', 'orderby' => 'meta_value_num', 'order' => 'DESC');
            break;
        default:
            $order_args = array('orderby' => 'date', 'order' => 'DESC');
    }
    
    // Формируем meta_query для фильтрации по цене
    $meta_query = array();
    if ($min_price !== '' || $max_price !== '') {
        $price_query = array('key' => '_price', 'compare' => 'BETWEEN', 'type' => 'DECIMAL');
        if ($min_price !== '') $price_query['value'][0] = $min_price;
        if ($max_price !== '') $price_query['value'][1] = $max_price;
        $meta_query[] = $price_query;
    }
    
    // Формируем tax_query для категорий и атрибутов
    $tax_query = array('relation' => 'AND');
    
    if (!empty($categories)) {
        $tax_query[] = array(
            'taxonomy' => 'product_cat',
            'field'    => 'slug',
            'terms'    => $categories,
            'operator' => 'IN'
        );
    }
    
    if (!empty($colors)) {
        $tax_query[] = array(
            'taxonomy' => 'pa_color',
            'field'    => 'slug',
            'terms'    => $colors,
            'operator' => 'IN'
        );
    }
    
    // Основной запрос
    $args = array(
        'post_type'      => 'product',
        'posts_per_page' => 12,
        'paged'          => $paged,
        'post_status'    => 'publish',
        'meta_query'     => $meta_query,
        'tax_query'      => $tax_query,
    );
    
    // Объединяем с аргументами сортировки
    $args = wp_parse_args($order_args, $args);
    
    $query = new WP_Query($args);
    
    ob_start();
    
    if ($query->have_posts()) {
        woocommerce_product_loop_start();
        
        while ($query->have_posts()) {
            $query->the_post();
            wc_get_template_part('content', 'product');
        }
        
        woocommerce_product_loop_end();
        
        // Пагинация
        woocommerce_pagination();
    } else {
        echo '<p class="no-products">Товаров по вашему запросу не найдено.</p>';
    }
    
    $html = ob_get_clean();
    wp_reset_postdata();
    
    // Формируем текст счётчика
    $count_text = sprintf(
        'Показано %d из %d товаров',
        min($query->post_count, 12),
        $query->found_posts
    );
    
    wp_send_json_success(array(
        'html'       => $html,
        'count_text' => $count_text,
        'total'      => $query->found_posts,
        'pages'      => $query->max_num_pages
    ));
}

// Два хука — для авторизованных и неавторизованных
add_action('wp_ajax_filter_products', 'wenesis_filter_products');
add_action('wp_ajax_nopriv_filter_products', 'wenesis_filter_products');

Важные детали, которые новички часто упускают:

  • nonce-проверка — без неё ваш фильтр = открытая дверь для ботов
  • sanitize_text_field и floatval — очистка входящих данных. Всегда
  • wp_reset_postdata() — без него глобальный $post «сломается», и следующий контент на странице выведет данные неправильного товара
  • Два хукаwp_ajax_ для залогиненных, wp_ajax_nopriv_ для гостей. Забыли nopriv — гости увидят 0

Шаг 4: Стили и анимация загрузки

Пока AJAX-фильтр для WooCommerce отправляет запрос, пользователь должен видеть, что «что-то происходит». Добавим простой лоадер:

/ Состояние загрузки /
.products.loading {
position: relative;
opacity: 0.5;
pointer-events: none;
transition: opacity 0.3s ease;
}

.products.loading::after {
content: »;
position: absolute;
top: 50%;
left: 50%;
width: 40px;
height: 40px;
margin: -20px 0 0 -20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #333;
border-radius: 50%;
animation: spin 0.8s linear infinite;
z-index:

Добавить комментарий

Разработка сайтов на Wordpress