Ajax-фильтр записей WordPress без плагинов

  1. Главная
  2. Блог
  3. WordPress
  4. Ajax-фильтр записей WordPress без плагинов

Всем привет!

Хочу рассказать об универсальном способе фильтровать любые записи в WordPress. Таким способом можно фильтровать как стандартные типы записей, так и кастомные, созданные вами или плагинами. Также этот способ подойдет для каталога в WooCommerce, но там можно все сделать немного проще.

Если интересно — пишите комментарии, может быть я напишу статью об этом и выпущу видео 🙂

С чем будем работать

У меня есть сайт на WordPress, с активной дочерней темой от TwentyTwenty. В этой теме я сгенерировал 20 записей типа post при помощи WP-CLI. У записей есть 4 рубрики: WordPress; CMS; ModX; CMS Winter. Есть также теги: темы; плагины; доработки; начинающим.

В случайном порядке я добавил эти записи к рубрикам и тегам. Также для примера добавлю мета-поле «Сложность»: высокая; средняя; низкая.

На примере рубрик и меток мы рассмотрим фильтрацию по таксономиям и терминам, а на примере мета-поля — фильтрацию по мета-полям. Аналогично можно фильтровать записи по мета-полям, созданным в ACF или Carbon Fields.

Шаг 1. Создание интерфейса

Для начала создадим интерфейс — то, с чем будет работать пользователь. Разместим его вверху страницы блога. Для этого скопируем файл index.php из родительской темы и добавим вверх экшен feodoraxis_posts_filter.

Если не знакомы с темой экшенов, хуков и фильтров в WordPress — то можете ознакомиться со статьёй «Хуки и фильтры WordPress для начинающих«.

Код скопированного index.php будет выглядеть так:

<?php
get_header();
?>

	<!-- Добавление экшена -->
	<div class="section-inner">
		
		<?php do_action( "feodoraxis_posts_filter" ); ?>
		
	</div>
	<!-- Добавление экшена (конец) -->

	<main id="site-content">

		<?php

		$archive_title    = '';
		$archive_subtitle = '';

		if ( is_search() ) {
			/**
			 * @global WP_Query $wp_query WordPress Query object.
			 */
			global $wp_query;

			$archive_title = sprintf(
				'%1$s %2$s',
				'<span class="color-accent">' . __( 'Search:', 'twentytwenty' ) . '</span>',
				'&ldquo;' . get_search_query() . '&rdquo;'
			);

			if ( $wp_query->found_posts ) {
				$archive_subtitle = sprintf(
				/* translators: %s: Number of search results. */
					_n(
						'We found %s result for your search.',
						'We found %s results for your search.',
						$wp_query->found_posts,
						'twentytwenty'
					),
					number_format_i18n( $wp_query->found_posts )
				);
			} else {
				$archive_subtitle = __( 'We could not find any results for your search. You can give it another try through the search form below.', 'twentytwenty' );
			}
		} elseif ( is_archive() && ! have_posts() ) {
			$archive_title = __( 'Nothing Found', 'twentytwenty' );
		} elseif ( ! is_home() ) {
			$archive_title    = get_the_archive_title();
			$archive_subtitle = get_the_archive_description();
		}

		if ( $archive_title || $archive_subtitle ) {
			?>

			<header class="archive-header has-text-align-center header-footer-group">

				<div class="archive-header-inner section-inner medium">

					<?php if ( $archive_title ) { ?>
						<h1 class="archive-title"><?php echo wp_kses_post( $archive_title ); ?></h1>
					<?php } ?>

					<?php if ( $archive_subtitle ) { ?>
						<div class="archive-subtitle section-inner thin max-percentage intro-text"><?php echo wp_kses_post( wpautop( $archive_subtitle ) ); ?></div>
					<?php } ?>

				</div><!-- .archive-header-inner -->

			</header><!-- .archive-header -->

			<?php
		}

		if ( have_posts() ) {

			$i = 0;

			while ( have_posts() ) {
				++$i;
				if ( $i > 1 ) {
					echo '<hr class="post-separator styled-separator is-style-wide section-inner" aria-hidden="true" />';
				}
				the_post();

				get_template_part( 'template-parts/content', get_post_type() );

			}
		} elseif ( is_search() ) {
			?>

			<div class="no-search-results-form section-inner thin">

				<?php
				get_search_form(
					array(
						'aria_label' => __( 'search again', 'twentytwenty' ),
					)
				);
				?>

			</div><!-- .no-search-results -->

			<?php
		}
		?>

		<?php get_template_part( 'template-parts/pagination' ); ?>

	</main><!-- #site-content -->

<?php get_template_part( 'template-parts/footer-menus-widgets' ); ?>

<?php
get_footer();

Теперь отобразим список полей для фильтрации. Для этого добавим хук к нашему экшену. Чтобы сделать это, в functions.php дочерней темы добавим следующий код:

<?php

function feodoraxis_posts_filter():void {
	global $wpdb;
	
	//Получим доступные для фильтрации категории
	$categories = get_terms( [
		'taxonomy' => 'category'
	] );

	//Получим доступные для фильтрации теги
	$tags = get_terms( [
		'taxonomy' => 'post_tag'
	] );

	//Получим доступные для фильтрации уровни сложности
	$sql = "SELECT `meta_value` FROM `{$wpdb->postmeta}` WHERE `meta_key` = 'Сложность' GROUP BY `meta_value`";
	$result = $wpdb->get_results( $sql, ARRAY_A );
	$difficulty_levels = array_column( $result, "meta_value" );

	get_template_part( 'template-parts/posts', 'filter', [
		'categories' => $categories ?? [],
		'tags' => $tags ?? [],
		'difficulty_levels' => $difficulty_levels ?? []
	] );
}
add_action( "feodoraxis_posts_filter", "feodoraxis_posts_filter" );

Обратите внимание, что здесь мы пока получаем только возможные доступные значения, по которым пользователь может фильтровать записи в блоге. Также по мета-полю, с помощью sql-запроса мы получаем список всех доступных вариантов сложности, чтобы не хардкодить их а дать возможность получить записи по реальным значениям.

Также обратите внимание, что в конце мы, с помощью функции get_template_part(), подключаем файл posts-filter.php в папке template-parts, и передаем в этот файл все полученные значения.

Создадим сам файл и сделаем в нём форму:

<?php
if ( ! defined( "ABSPATH" ) ) {
	exit;
}

/**
 * @var array{categories:WP_Term[], tags:WP_Term[], difficulty_levels:string[]} $args
 */
?>

<form action="<?php echo $_SERVER['REQUEST_URI']; ?>" method="post" data-action="<?php echo admin_url( "admin-ajax.php" ); ?>" id="feodoraxis_posts_filter_form">
	<ul class="post-meta">

		<?php if ( is_array( $args['categories'] ) && ! empty( $args['categories'] ) ) : ?>

			<li>
				<p>Категории:</p>

				<div class="feodoraxis-flex">
					<?php foreach ( $args['categories'] as $category ) : ?>

						<div class="feodoraxis-flex">
							<input type="radio" name="category" value="<?php echo $category->term_id; ?>" id="category_<?php echo $category->term_id; ?>" />
							<label for="category_<?php echo $category->term_id; ?>"><?php echo $category->name; ?></label>
						</div>

					<?php endforeach; ?>
				</div>

			</li>

		<?php endif; ?>

		<?php if ( is_array( $args['tags'] ) && ! empty( $args['tags'] ) ) : ?>

			<li>
				<p>Теги:</p>

				<?php foreach ( $args['tags'] as $tag ) : ?>

					<input type="checkbox" name="tag[]" value="<?php echo $tag->term_id; ?>" id="tag_<?php echo $tag->term_id; ?>" />
					<label for="tag_<?php echo $tag->term_id; ?>"><?php echo $tag->name; ?></label>

				<?php endforeach; ?>

			</li>

		<?php endif; ?>

		<?php if ( is_array( $args['difficulty_levels'] ) && ! empty( $args['difficulty_levels'] ) ) : ?>

			<li>
				<p>Сложность:</p>

				<select name="difficulty_level" id="">
					<option value="">-- Выбрать --</option>
					<?php foreach ( $args['difficulty_levels'] as $difficulty_level ) : ?>

						<option value="<?php echo $difficulty_level; ?>"><?php echo $difficulty_level; ?></option>

					<?php endforeach; ?>
				</select>

			</li>

		<?php endif; ?>

		<li>
            		<button class="wp-block-search__button wp-element-button">Отправить</button>
	        </li>

	</ul>
</form>

Для блоков с классом .feodoraxis-flex я добавил такие стили:

.feodoraxis-flex {
    display: flex;
    align-items: center;
}

.feodoraxis-flex label {
    margin: 0;
}

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

Мы создали интерфейс. Отлично!

Переходим к следующему шагу.

Шаг 2. Отправка Ajax-запроса

Если вы плохо знакомы с темой Ajax в WordPress, то можете ознакомиться со статьёй Ajax в WordPress

Для начала создадим файл main.js в корне дочерней темы и подключим его. Для этого добавим такой код в файл functions.php:

<?php
function feodoraxis_enqueue_script():void {
	// Обратите внимание, что мы сначала выходим из родительской темы и потом заходим в дочернюю, т.к. функция get_template_directory_uri() по-умолчанию ведет в родительскую тему
	wp_enqueue_script( 'feodoraxis-main', get_template_directory_uri() . '/../twentytwenty-child/main.js', [], '2.0' );
}
add_action( 'wp_enqueue_scripts', 'feodoraxis_enqueue_script' );

Чтобы отправить ajax-запрос, добавим такой JavaScript-код:

document.addEventListener("DOMContentLoaded", () => {

    const feodoraxisPostsFilterForm = document.querySelector("#feodoraxis_posts_filter_form");
    if ( ! feodoraxisPostsFilterForm ) {
        return;
    }

    //Добавим событие отправки формы
    feodoraxisPostsFilterForm.addEventListener("submit", (e) => {

        //Отменим стандартное поведение формы при отправке
        e.preventDefault();

        //Получим ссылку, по которой нужно сделать запрос
        const adminAjax = feodoraxisPostsFilterForm.dataset.action;

        //Получим данные из формы
        const formData = new FormData(feodoraxisPostsFilterForm);

        //Добавим в данные информацию об ajax-экшене, который должен исполниться при запросе
        formData.append('action', 'feodoraxis_posts_filter_request' );

        //Создадим запрос
        const request = new XMLHttpRequest();

        // Откроем запрос
        request.open("POST", adminAjax);

        //Укажем заголовки запроса
        request.setRequestHeader("Accept", "application/json");

        //Отправим данные из формы
        request.send(formData);

        //Когда запрос выполнится - обновим список записей
        request.onload = () => {
            const resultJson = request.responseText;
            const result = JSON.parse(resultJson);

            document.querySelector("#site-content").innerHTML = result.data.posts;
        };

        return false;
    })
});

Шаг 3. Обработка ajax-запроса

Теперь в functions.php добавим код для обработки этого запроса:

function feodoraxis_posts_filter_request():void {
	if ( $_SERVER['REQUEST_METHOD'] != "POST" || ! wp_doing_ajax() ) {
		wp_send_json_error();
	}
	
	$args = [
		'post_type' => 'post'
	];

	if ( isset( $_POST['category'] ) && ! empty( $_POST['category'] ) ) {
		$args['tax_query'][] = [
			'taxonomy' => 'category',
			'field' => 'id',
			'terms' => intval( $_POST['category'] ),
		];
	}

	if ( isset( $_POST['tag'] ) && ! empty( $_POST['tag'] ) ) {
		$tags = array_map( function( $tag ) {
			return intval( $tag );
		}, $_POST['tag'] );

		$args['tax_query'][] = [
			'taxonomy' => 'post_tag',
			'field' => 'id',
			'terms' => $tags,
		];
	}

	if ( isset( $_POST['difficulty_level'] ) && ! empty( $_POST['difficulty_level'] ) ) {
		$difficulty_level = sanitize_text_field( $_POST['difficulty_level'] );

		$args['meta_query'][] = [
			'key' => 'Сложность',
			'value' => $difficulty_level
		];
	}

	$query = new WP_Query( $args );

	ob_start();
	if ( $query->have_posts() ) {
		$i = 0;

		while ( $query->have_posts() ) {
			++$i;
			if ( $i > 1 ) {
				echo '<hr class="post-separator styled-separator is-style-wide section-inner" aria-hidden="true" />';
			}
			$query->the_post();

			get_template_part( 'template-parts/content', 'post' );
		}
	} else {
		echo '<p>К сожалению, по вашему запросу ничего не найдено</p>';
	}

	$content = ob_get_contents();
	ob_clean();

	wp_reset_postdata();

	wp_send_json_success( [
		'posts' => $content,
	] );
}
add_action( 'wp_ajax_' . 'feodoraxis_posts_filter_request', 'feodoraxis_posts_filter_request' );
add_action( 'wp_ajax_nopriv_' . 'feodoraxis_posts_filter_request', 'feodoraxis_posts_filter_request' );

Отлично! Если вы все сделали правильно, то после этого у вас должен заработать фильтр!

Завершение

Надеюсь, что статья была вам полезна. Обязательно пишите комментарии и задавайте вопросы. Буду рад на них ответить 🙂

Желаю вам успехов!