Filtrer ses articles et ajouter un bouton « loadmore »

Comment créer un filtre et un bouton ajax load more pour vos articles ?

Aujourd’hui on va voir comment créer un système de filtre (classification) pour les articles, et ajouter une navigation ajax grâce à un bouton load more.

Les sources

Le code s’inspire très largement de l’excellent tuto de rudrastyh, sauf que nous on ne va pas utiliser le même système de filtres. Dans son tutoriel, rudrastyh utilise des champs select qu’il relie entre eux. Dans notre exemple, on va partir sur des checkbox ou boutons radios, stylisés en bouton. Pour une simple liste en ul>li, le code sera le même.

Je me suis également appuyé sur le tuto d’Alicia ramirez, qui en fait une utilisation plus simple en utilisant isotope.js.

On va partir ici d’un cadre relativement simple, celui des articles classiques de wordpress.

Un affichage original

On va en plus permettre un affichage original de nos articles, avec un système en colonnes, qui se reproduira à chaque filtrage .

Voici donc le résultat auquel on veut parvenir :

filtres ajax wordpress

L’originalité de mon exemple, c’est qu’au clic sur n’importe lequel des filtres, la présentation des vignettes d’articles sera la même, avec toujours en premier, une vignette de 12 colonnes, ensuite 6, puis 4.

Ce dont on va avoir besoin :

  • Des articles. Cela paraît évident, donc il faut créer des articles pour que le système fonctionne.
  • Des catégories : liez vos articles à des catégories, les boutons des filtres représentent justement vos catégories.
  • Une grille en colonne, j’utilise ici une grille flexbox originale que vous pouvez trouver ici.

La structure de base

Je repars ici de mon exemple précédent, auquel je rajoute le système de filtres. Je suis dans ma page blog, donc dans mon fichier index.php ou home.php.

<div class="flex-container">
<div class="flex-row">
<header class="page-header">
<?php
single_post_title('<h1 class="page-title">', '</h1>' );
?>
</header><!-- .page-header -->
</div>
<div class="flex-row">
<form action="#" method="POST" id="post_filters">
<p><input type="radio" value="all" id="all" class="category_filter" name="category_filters"><label for="all">Toutes</label></p>
<?php
if( $terms = get_terms( array( 'taxonomy' => 'category' ) ) ) :
foreach( $terms as $term ) :
echo '<p><input type="radio" id="' . $term->term_id . '" value="' . $term->term_id . '" name="category_filters" class="category_filter"/><label for="' . $term->term_id. '">' . $term->name . '</label></p>';
endforeach;
endif;
?>
<!-- required hidden field for admin-ajax.php -->
<input type="hidden" name="action" value="ccfilter" />
</form>
</div>
<?php
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$args = array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 5,
'paged' => $paged

);
$query = new WP_Query( $args ); ?>
<?php
if ( $query->have_posts() ) :
$count = (int)0;?>
<div id="cc_posts_wrap" class="flex-row">
<?php
while ( $query->have_posts() ) : $count++;
$query->the_post();
if($count == 1){ // Si c'est le premier article de la liste, entour le d'une clase 12 colonnes
$span = 'flex-col-xs-12';
}
if($count == 2 || $count == 3){// Si c'est le deuxième et troisième article de la liste, entour le d'une clase 6 colonnes
$span = 'flex-col-sm-6';

}
if($count > 3){ // sinon pour tous les suivants, répartis les en 3 colonnes
$span = 'flex-col-sm-4';

}
//If its not 3 or higher, increase the count
$termsArray = get_the_terms($post->ID, "category");
$termsString ="";
foreach ( $termsArray as $term ) {
$termsString .= $term->slug;
}
?>
<div class="<?php echo $termsString .' ' . $span ;?> item">
<article id="post-<?php the_ID(); ?>" <?php post_class();?>>
<div class="post-featured-thumbnail">
<?php
if ( has_post_thumbnail() ) {
if($count == 1){
the_post_thumbnail('blog_featured');
}else{
the_post_thumbnail();
}
}

if ( 'post' === get_post_type() ) :
?>
<div class="entry-meta">
<?php
numgrade_category_sticker();
?>
</div><!-- .entry-meta -->

<?php endif; ?>
</div>

<div class="post-content">
<header class="entry-header">
<?php
if ( is_singular() ) :
the_title( '<h1 class="entry-title">', '</h1>' );
else :
the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
endif;
?>
</header><!-- .entry-header -->
<div class="entry-content">
<?php the_excerpt(); ?>
</div><!-- .entry-content -->
<?php
if($count == 1){ ?>
<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Lire</a>
</div>
<?php } ?>
</div>
<?php if($count != 1){ ?>
<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Lire</a>
</div>
<?php } ?>
</article><!-- #post-<?php the_ID(); ?> -->
</div>


<?php endwhile;?>
</div> <!-- end isotope-list -->

<?php global $wp_query;
if ( $wp_query->max_num_pages > 1 ) :
echo '<div class="loadmore_block"><div id="cc_loadmore">Voir plus d\'articles</div></div>';
endif;

else :

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

endif;
?>
</div><!-- .flex-container -->

Le système de filtres

Le système de filtres est assez simple, c’est un code que l’on peut retrouver un peu partout, notamment sur stackoverflow, puisque tout le monde se base sur les mêmes tutoriels, celui d’Alicia Ramirez par exemple ! C’est évidemment à personnaliser en fonction de ses besoins.

<form action="#" method="POST" id="post_filters">
<p><input type="radio" value="all" id="all" class="category_filter" name="category_filters"><label for="all">Toutes</label></p>
<?php
if( $terms = get_terms( array( 'taxonomy' => 'category' ) ) ) :
foreach( $terms as $term ) :
echo '<p><input type="radio" id="' . $term->term_id . '" value="' . $term->term_id . '" name="category_filters" class="category_filter"/><label for="' . $term->term_id. '">' . $term->name . '</label></p>';
endforeach;
endif;
?>
<!-- required hidden field for admin-ajax.php -->
<input type="hidden" name="action" value="ccfilter" />
</form>

C’est donc un simple formulaire, contenant un bouton radio « all » censé réinitialiser la recherche ou le filtrage. Ensuite dans une boucle on crée autant de boutons radios qu’il y a de catégories, et pour chacune d’entre elle, on récupère l’id, le nom ou le slug.

Il faut faire très attention aux noms données aux éléments : Choisissez des noms précis et gardez-les, il faut avoir un peu de rigueur là-dessus car on va les retrouver un peu partout dans le système :

  • l’ID du formulaire : id= »post_filters »
  • le nom de nos boutons radio : name= »category_filters »
  • la valeur du champ caché pour envoyer le formulaire: value= »ccfilter »

L’article en lui-même

La boucle principale va donc nous ramener nos articles. Ici, comme précisé au début, j’affiche grâce à un compteur que j’incrémente, une disposition en colonne, comme sur l’image. Le code est propre à mon projet, à vous de styliser ça à votre façon !

L’essentiel, c’est que l’article soit disposé dans un conteneur, ici je lui donne l’ID: id= »cc_posts_wrap ». Le système de filtres, ainsi que le bouton load more sont donc à l’extérieur de ce conteneur.

Le bouton load-more

Enfin, tout à la fin on place notre bouton load more

<?php	global $wp_query; 
if ( $wp_query->max_num_pages > 1 ) :
echo '<div class="loadmore_block"><div id="cc_loadmore">Voir plus d\'articles</div></div>';

Le moteur des filtres et bu bouton ajax

Voici tout le code à insérer dans functions.php

/*FUNCTION FILTER AND AJAX LOAD MORE*/
add_action( 'wp_enqueue_scripts', 'cc_script_and_styles');
function cc_script_and_styles() {
if ( is_home() || is_category() || is_archive()) {
global $wp_query;
wp_register_script( 'cc_scripts', get_stylesheet_directory_uri() . '/js/script.js', array('jquery') );
wp_localize_script( 'cc_scripts', 'cc_loadmore_params', array(
'ajaxurl' => site_url() . '/wp-admin/admin-ajax.php', // WordPress AJAX
'posts' => json_encode( $wp_query->query_vars ), // everything about your loop is here
'current_page' => $wp_query->query_vars['paged'] ? $wp_query->query_vars['paged'] : 1,
'max_page' => $wp_query->max_num_pages
) );

wp_enqueue_script( 'cc_scripts' );
}
}
/*AJAX LOAD MORE AND FILTERS*/
add_action('wp_ajax_loadmorebutton', 'cc_loadmore_ajax_handler');
add_action('wp_ajax_nopriv_loadmorebutton', 'cc_loadmore_ajax_handler');

function cc_loadmore_ajax_handler(){
$params = json_decode( stripslashes( $_POST['query'] ), true );
$params['paged'] = $_POST['page'] + 1;
$params['post_status'] = 'publish';
query_posts( $params );

if( have_posts() ) :
while( have_posts() ): the_post();
$termsArray = get_the_terms($post->ID, "category");
$termsString ="";
foreach ( $termsArray as $term ) {
$termsString .= $term->slug;
}
?>
<div class="<?php echo $termsString ;?> flex-col-sm-4 item">
<article id="post-<?php the_ID(); ?>" <?php post_class();?>>
<div class="post-featured-thumbnail">
<?php
if ( has_post_thumbnail() ) {

the_post_thumbnail();
}
if ( 'post' === get_post_type() ) :
?>
<div class="entry-meta">
<?php
numgrade_category_sticker();
?>
</div><!-- .entry-meta -->
<?php endif; ?>
</div>
<div class="post-content">
<header class="entry-header">
<?php
the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
?>
</header><!-- .entry-header -->
<div class="entry-content">
<?php the_excerpt(); ?>
</div><!-- .entry-content -->
</div>
<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Lire</a>
</div>
</article><!-- #post-<?php the_ID(); ?> -->
</div>
<?php
endwhile;
endif;
die;
}

add_action('wp_ajax_ccfilter', 'cc_filter_function');
add_action('wp_ajax_nopriv_ccfilter', 'cc_filter_function');

function cc_filter_function(){
if( isset( $_POST['all'] ) )
$terms = get_terms( array( 'taxonomy' => 'category' ) );
$args['tax_query'] = array(
array(
'taxonomy' => 'category',
'field' => 'id',
'terms' => $terms,
"posts_per_page" => 5
)
);
if( isset( $_POST['category_filters'] ) )
$args['tax_query'] = array(
array(
'taxonomy' => 'category',
'field' => 'id',
'terms' => $_POST['category_filters'],
"posts_per_page" => 5
)
);
query_posts( $args );
global $wp_query;
if( have_posts() ) : $count = (int)0;
ob_start();
while( have_posts() ): $count++;
the_post();
if($count == 1){
$span = 'flex-col-xs-12';
}
if($count == 2 || $count == 3){
$span = 'flex-col-sm-6';
}
if($count > 3){
$span = 'flex-col-sm-4';
}
$termsArray = get_the_terms($post->ID, "category");
$termsString ="";
foreach ( $termsArray as $term ) {
$termsString .= $term->slug;
}
?>
<div class="<?php echo $termsString .' ' . $span ;?> item">
<article id="post-<?php the_ID(); ?>" <?php post_class();?>>
<div class="post-featured-thumbnail">
<?php
if ( has_post_thumbnail() ) {
if($count == 1){
the_post_thumbnail('blog_featured');
}else{
the_post_thumbnail();
}
}
if ( 'post' === get_post_type() ) :
?>
<div class="entry-meta">
<?php
numgrade_category_sticker();
?>
</div><!-- .entry-meta -->
<?php endif; ?>
</div>
<div class="post-content">
<header class="entry-header">
<?php
if ( is_singular() ) :
the_title( '<h1 class="entry-title">', '</h1>' );
else :
the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
endif;
?>
</header><!-- .entry-header -->
<div class="entry-content">
<?php the_excerpt(); ?>
</div><!-- .entry-content -->
<?php
if($count == 1){ ?>
<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Lire</a>
</div>
<?php } ?>
</div>
<?php if($count != 1){ ?>
<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Lire</a>
</div>
<?php } ?>
</article><!-- #post-<?php the_ID(); ?> -->
</div>
<?php
endwhile;
$posts_html = ob_get_contents();
ob_end_clean();
else:
$posts_html = '<p>Aucun résultat</p>';
endif;

echo json_encode( array(
'posts' => json_encode( $wp_query->query_vars ),
'max_page' => $wp_query->max_num_pages,
'found_posts' => $wp_query->found_posts,
'content' => $posts_html
) );
die();
}

Le code est à diviser en trois étapes:

1-D’abord, l’appel aux scripts ajax de wordpress

add_action( 'wp_enqueue_scripts', 'cc_script_and_styles');
function cc_script_and_styles() {
if ( is_home() || is_category() || is_archive()) {
global $wp_query;
wp_register_script( 'cc_scripts', get_stylesheet_directory_uri() . '/js/script.js', array('jquery') );
wp_localize_script( 'cc_scripts', 'cc_loadmore_params', array(
'ajaxurl' => site_url() . '/wp-admin/admin-ajax.php', // WordPress AJAX
'posts' => json_encode( $wp_query->query_vars ), // everything about your loop is here
'current_page' => $wp_query->query_vars['paged'] ? $wp_query->query_vars['paged'] : 1,
'max_page' => $wp_query->max_num_pages
) );

wp_enqueue_script( 'cc_scripts' );
}
}

/*AJAX LOAD MORE AND FILTERS*/
add_action('wp_ajax_loadmorebutton', 'cc_loadmore_ajax_handler');
add_action('wp_ajax_nopriv_loadmorebutton', 'cc_loadmore_ajax_handler');

Dans notre fonction wp_enqueue_script, on fait appel à un premier fichier js qui va contenir tout le code nécessaire au bon fonctionnement du système de filtre et du rechargement ajax des articles. On voit ca juste après.

Puis on charge le fichier ‘admin-ajax.php‘ intégré par défaut dans wordpress.

Vous pouvez voir que j’entoure l’appel au script d’une condition :

if ( is_home() || is_category() || is_archive()) {...}

Comme j’utilise ce système aussi pour des custom post type, je limite ici l’appel des scripts au blog, aux catégories et aux archives puisque j’utilise ce même script pour mes catégories et mes archives. Comme l’appel au script ajax est différent pour les custom post type, j’évite ainsi une interférence entre les deux.

2-Ajax load more button

Les deux lignes de codes qui suivent nous servent à créer nos actions ajax. Les premières serviront au bouton ajax load more, et plus bas dans le code, on va retrouver la même chose mais cette fois pour les filtres ajax.

Dans notre add_action et pour faire fonctionner notre ajax, les deux « mots clés » wp_ajax_ » et « wp_ajax_nopriv_ » sont obligatoires, et ils doivent être suivis du nom de votre action.

Pour le bouton load_more, je leur donne un nom significatif «  loadmorebutton« . Le nom donné en deuxième argument : « cc_loadmore_ajax_handler » est le nom de la fonction située juste en dessous.

Comme on le voit, cette fonction « function cc_loadmore_ajax_handler(){ } » contient une boucle pour afficher mes articles. Ici, je n’ai pas besoin de reproduire l’affichage avec différentes tailles de colonnes. Comme les articles doivent s’afficher à la suite, je souhaite plutôt conserver un affichage homogène avec les derniers articles. Ils s’afficheront donc normalement, c’est à dire en 3 colonnes.

Filters functions

Ensuite vient le code pour le filtrage des articles.

Ici, on retrouve les deux mots clés obligatoires, suivi du nom de mes actions :

add_action('wp_ajax_ccfilter', 'cc_filter_function'); 
add_action('wp_ajax_nopriv_ccfilter', 'cc_filter_function');

Le nom de l’action donné ici:  « ccfilter« , doit être le même que celui de la valeur de mon champs caché dans le formulaire de filtrage, souvenez-vous.

	<!-- required hidden field for admin-ajax.php -->
<input type="hidden" name="action" value="ccfilter" />

Comme à chaque clic sur un bouton de filtrage la page devra « effacer les articles » pour afficher ceux d’une autre catégorie, je souhaite conserver mon affichage original montré dans l’image. La boucle est donc exactement la même que dans mon fichier index.php (ou home.php)

Pour le bouton « all », c’est assez simple en fait, on crée un boucle sans distinction de catégorie :

On ramène toutes les catégories, avec get_terms suivi du nom de la taxonomie qui est ici category (système classique de wordpress). On aurait aussi pu faire un simple get_categories().

$terms = get_terms( array( 'taxonomy' => 'category' ) );

Dans les arguments de la requête, on ne fait pas de distinction :

'terms' => $terms,

Dans la boucle suivante par contre, on fait en sorte de distinguer chaque catégorie avec un tax_query().

tax_query:: $args['tax_query'] = array(
array(

et dans les arguments, on ramène la catégorie qui a été sélectionnée par l’utilisateur :

'terms' => $_POST['category_filters'],

Notez que cette deuxième grande boucle est entourée des tags

ob_start(); 
(...)
$posts_html = ob_get_contents();
ob_end_clean();
(...)


echo json_encode( array(
'posts' => json_encode( $wp_query->query_vars ),
'max_page' => $wp_query->max_num_pages,
'found_posts' => $wp_query->found_posts,
'content' => $posts_html
) );

die();

Vous pouvez vous référer à l’article cité ou aller sur php.net pour comprendre l’utilité des fonctions ob_start(); ob_end_clean(); et ob_get_contents(); 

Les fonctions js

Dans notre fonction wp_enqueue_script, on a vu qu’on faisait appel à un fichier js

wp_register_script( 'cc_scripts', get_stylesheet_directory_uri() . '/js/script.js', array('jquery') );

Il va donc falloir créer ce fichier. Donnez-lui le nom que vous voulez, bien sûr le même que dans cette fonction wp_register_script().

Et voici donc tout le code à insérer dans le fichier js

 jQuery(function($){

/* LOAD MORE FUNCTION ON FORMATION ARCHIVE PAGE */
$('#cc_loadmore').click(function(){

$.ajax({
url : cc_loadmore_params.ajaxurl, // AJAX handler
data : {
'action': 'loadmorebutton', // le paramètre pour admin-ajax.php
'query': cc_loadmore_params.posts, // le paramètre de la loop utilisée par wp_localize_script()
'page' : cc_loadmore_params.current_page // la page en cours
},
type : 'POST',
beforeSend : function ( xhr ) {
$('#cc_loadmore').text('Recherche...'); // mettre ici let exte de son choix pour le chargement des articles
},
success : function( posts ){
if( posts ) {

$('#cc_loadmore').text( 'Voir plus d\'articles' );
$('#cc_posts_wrap').append( posts ); // on "append" les articles supplémentaires
cc_loadmore_params.current_page++;

if ( cc_loadmore_params.current_page == cc_loadmore_params.max_page )
$('#cc_loadmore').hide(); // on cache le bouton s'il n'y a plus d'articles

} else {
$('#cc_loadmore').hide(); // s'il n'y a rien à afficher on cache le bouton
}
}
});
return false;
});
/* FILTERING FUNCTION ON FORMATION ARCHIVE PAGE */
$('#post_filters').change(function(){

$.ajax({
url : cc_loadmore_params.ajaxurl,
data : $('#post_filters').serialize(), // form data
dataType : 'json', // on utilise le format json pour passer directement côté serveur
type : 'POST',

success : function( data ){

cc_loadmore_params.current_page = 1;

cc_loadmore_params.posts = data.posts;

cc_loadmore_params.max_page = data.max_page;

$('#cc_posts_wrap').html(data.content);

if ( data.max_page < 2 ) {
$('#cc_loadmore').hide();
} else {
$('#cc_loadmore').show();
}
}
});
// return folse pour ne pas envoyer le formulaire
return false;

});
});

Comme on le voit ici , on retrouve les différents noms donnés à nos élements :

#cc_loadmore c’est l’ID de notre bouton ajax load more dans notre fichier index.php ou home.php.

‘action’: ‘loadmorebutton’, c’est donc le nom de notre action ajax,

cc_loadmore_params c’est le nom donné en argument à notre fonction wp_localize_script()

#cc_posts_wrap c’est le nom de notre conteneur principal, dans notre fichier index.php ou home.php.

#post_filters, c’est l’ID de notre formulaire de filtrage.

Utiliser le même système pour ses pages d’archives

Pour aller plus loin, vous pouvez aussi faire fonctionner votre bouton ajax load more dans vos pages d’archives.

Je mets ici tout le code d’une page archive, dans laquelle j’intègre ce bouton. C’est le même css finalement que dans la page blog, et on utilise le même appel au script js. J’utilise donc la fonction function cc_loadmore_ajax_handler(){} et la boucle contenue à l’intérieur

<?php get_header(); ?>

<div id="primary" class="content-area">
<main id="main" class="site-main">
<div class="flex-container">
<div class="flex-row">
<?php if ( have_posts() ) : ?>

<header class="page-header">
<?php
the_archive_title( '<h1 class="page-title">', '</h1>' );
the_archive_description( '<div class="archive-description">', '</div>' );
?>
</header><!-- .page-header -->
</div>

<div id="cc_posts_wrap" class="flex-row">
<?php
/* Start the Loop */
while ( have_posts() ) : the_post(); ?>

<div class="flex-col-sm-6 flex-col-md-4 item">
<article id="post-<?php the_ID(); ?>" <?php post_class();?>>
<div class="post-featured-thumbnail">
<?php
if ( has_post_thumbnail() ) {
if($count == 1){
the_post_thumbnail('blog_featured');
}else{
the_post_thumbnail();
}
}

if ( 'post' === get_post_type() ) :
?>
<div class="entry-meta">
<?php
numgrade_category_sticker();
?>
</div><!-- .entry-meta -->

<?php endif; ?>
</div>

<div class="post-content">
<header class="entry-header">
<?php
if ( is_singular() ) :
the_title( '<h1 class="entry-title">', '</h1>' );
else :
the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
endif;
?>
</header><!-- .entry-header -->
<div class="entry-content">
<?php the_excerpt(); ?>
</div><!-- .entry-content -->


</div>

<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Lire</a>
</div>

</article><!-- #post-<?php the_ID(); ?> -->
</div>

<?php endwhile;?>
</div> <!-- end isotope-list -->
<?php
global $wp_query; // you can remove this line if everything works for you
if ( $wp_query->max_num_pages > 1 ) :
echo '<div class="loadmore_block"><div id="cc_loadmore">Voir plus d\'articles</div></div>'; // you can use <a> as well
endif;
else :
get_template_part( 'template-parts/content', 'none' );

endif;
?>
</div><!-- .flex-container -->
</main><!-- #main -->
</div><!-- #primary -->

<?php

get_footer();?>

Dans un prochain article, on va voir comment pousser encore plus loin et faisant la même chose avec les custom  post type.

Recommandations :

Si malgré tout ce code vous ne parvenez pas à faire marcher le bouton loadmore en ajax ou le système de filtres, je vous invite à consulter les nombreux commentaires sur les pages suivantes :  loadmore, filtersajax et filtres, load more et pagination. De nombreux internautes posent des questions et d’autres y répondent. Vous devriez y trouver des pistes !

Si vous rencontrez un problème lors de l’affichage des articles, notamment au niveau du nombre de posts affichés, changez le paramètre « posts_per_page » et modifiez également les options dans le backoffice de wordpress dans Réglages / Lecture.