Filtrer ses custom post type et ajouter un bouton load more

Comment rajouter des filtres et un bouton load more à ses custom post type ?

Dans un précédent article, j’ai montré un exemple d’implémentation, à partir des articles classiques, d’une fonction de filtrage et de rechargement en ajax des articles.

Ici on va voir comment pousser encore plus loin ce système en l’étendant aux custom post type. Voici un rendu possible : on a nos différents filtres tout en haut, et un bouton de rechargement d’articles en ajax tout en bas.

filtres cpt wordpress

Création du custom post type

Je pars du même exemple que précédemment : je crée un custom post type appelé « formation ». Je lui attribue deux taxonomies : « categorie-formations » et « niveau-formation« . Ces taxonomies ont comme argument : ‘hierarchical’ => true », : cela permet entre autre d’avoir des cases à cocher dans le backoffice, comme pour des catégories classiques.

<?php
function cc_formation_cpt() {
/* Formation */
$labels = array(
'name' => _x('Formations', 'Post Type General Name', 'text_domain'),
'singular_name' => _x('Formation', 'Post Type Singular Name', 'text_domain'),
'menu_name' => _x('Formations', 'text_domain'),
'name_admin_bar' => _x('Formations', 'text_domain'),
'all_items' => _x('Toutes les formations', 'text_domain'),
'add_new_item' => _x('Ajouter une nouvelle formation', 'text_domain'),
'add_new' => _x('Ajouter', 'text_domain'),
'new_item' => _x('Nouvelle formation', 'text_domain' ),
'edit_item' => _x('Editer la formation', 'text_domain'),
'update_item' => _x('Mettre à jour la formation', 'text_domain'),
'view_item' => _x('Voir la formation', 'text_domain'),
'search_items' => _x('Rechercher une formation', 'text_domain'),
'not_found' => _x('Aucune formation trouvée', 'text_domain'),
'not_found_in_trash' => _x('Aucune formation trouvée dans la corbeille', 'text_domain'),
);

$args = array(
'label' => _x('Formation', 'text_domain'),
'description' => _x('Formations', 'text_domain'),
'labels' => $labels,
'supports' => array('title', 'editor', 'thumbnail', 'comments', 'revisions', 'custom-fields'),
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'menu_position' => 5,
'menu_icon' => 'dashicons-admin-home',
'show_in_admin_bar' => true,
'show_in_nav_menus' => true,
'can_export' => true,
'has_archive' => false,
'exclude_from_search' => false,
'publicly_queryable' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'toutes-nos-formations' ),
'capability_type' => 'post',
'show_in_rest' =>'true',
);
register_post_type('formation', $args);
}
add_action('init', 'cc_formation_cpt', 10);



function cc_formation_category_taxonomies() {

$labels_cat_formation = array(
'name' => _x( 'Catégories de la formation', 'taxonomy general name'),
'singular_name' => _x( 'Catégories de la formation', 'taxonomy singular name'),
'search_items' => __( 'Rechercher une catégorie'),
'popular_items' => __( 'Catégories populaires'),
'all_items' => __( 'Toutes les catégories'),
'edit_item' => __( 'Editer une catégorie'),
'update_item' => __( 'Mettre à jour une catégorie'),
'add_new_item' => __( 'Ajouter une nouvelle catégorie'),
'new_item_name' => __( 'Nom de la nouvelle catégorie'),
'add_or_remove_items' => __( 'Ajouter ou supprimer une catégorie'),
'choose_from_most_used' => __( 'Choisir parmi les catégories les plus utilisées'),
'not_found' => __( 'Pas de catégories trouvée'),
'menu_name' => __( 'Catégorie de formation'),
);

$args_cat_formation = array(
'hierarchical' => true,
'labels' => $labels_cat_formation,
'show_ui' => true,
'show_in_rest' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'categories-formations' ),
);

register_taxonomy( 'categories-formations', 'formation', $args_cat_formation );
}
add_action( 'init', 'cc_formation_category_taxonomies', 0 );

function cc_formation_level_taxonomies() {

$labels_level_formation = array(
'name' => _x( 'Niveaux de la formation', 'taxonomy general name'),
'singular_name' => _x( 'Niveau de la formation', 'taxonomy singular name'),
'search_items' => __( 'Rechercher un niveau'),
'popular_items' => __( 'Niveaux populaires'),
'all_items' => __( 'Tous les Niveaux'),
'edit_item' => __( 'Editer un niveau'),
'update_item' => __( 'Mettre à jour un niveau'),
'add_new_item' => __( 'Ajouter un nouveau niveau'),
'new_item_name' => __( 'Nom de le nouveau niveau'),
'add_or_remove_items' => __( 'Ajouter ou supprimer un niveau'),
'choose_from_most_used' => __( 'Choisir parmi les nivexau les plus utilisées'),
'not_found' => __( 'Pas de niveau trouvé'),
'menu_name' => __( 'Niveau de formation'),
);

$args_level_formation = array(

'hierarchical' => true,
'labels' => $labels_level_formation,
'show_ui' => true,
'show_in_rest' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'niveau-formations' ),
);

register_taxonomy( 'niveau-formations', 'formation', $args_level_formation );
}
add_action( 'init', 'cc_formation_level_taxonomies', 0 );

Une page d’archive pour le custom post type

Je crée ensuite ma page d’archive page-formation.php pour mon custom post type. Normalement, en passant l’argument has_archive à true, wordpress va automatiquement générer une page d’archive pour notre custom post type. Il suffira alors d’aller la chercher dans Apparence / Menu et de la placer dans le menu. Le problème c’est que cette page est « fictive », dans le sens où elle n’existe pas en dur dans le dossier wordpress. On ne peut donc pas l’administrer.

Dans mon exemple, j’ai forcément besoin de pouvoir la personnaliser puisque je veux y inclure mon système de filtre et le bouton ajax load more. Je veux aussi donner la possibilité à l’utilisateur final de pouvoir intervenir dessus. Après le bloc de filtre et la boucle pour mes custom post type, je replace donc un the_content(), ce qui va lui permettre d’éditer la page via l’éditeur classique de wordpress, ou tout autre page builder.

<?php
/**
* Template Name: Toutes nos Formations
*
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/

*
*/
get_header();
?>

<div id="primary" class="content-area">
<main id="main" class="site-main">
<div class="flex-container">
<div class="flex-row">
<header class="page-header">
<h1 class="archive_title"><?php // Ici je ramène deux champs additionnels crées avec ACF, afin de laisser l'utilisateur indiquer un titre et une description personnalisée à mon modèle de page
if(get_field('titre'))
{
echo '<p>' . get_field('titre') . '</p>';
}
?>
</h1>
<h2>
<?php if(get_field('description')){
echo '<p>' . get_field('description') . '</p>';
} ?>
</h2>

</header><!-- .page-header -->
</div>
<div class="flex-row"> <?php // Voici le formulaire de filtrage. On boucle directement sur les taxonomie de mon custom post type ?>
<form action="#" method="POST" id="formation_filters">
<p><input type="radio" value="all" id="all" class="category_filter" name="category_formation_filters"><label for="all">Toutes</label></p>

<?php
if( $terms = get_terms( array( 'taxonomy' => 'categories-formations' ) ) ) :
foreach( $terms as $term ) :
echo '<p><input type="radio" id="' . $term->term_id . '" value="' . $term->term_id . '" name="category_formation_filters" class="category_formation_filters"/><label for="' . $term->term_id. '">' . $term->name . '</label></p>';
endforeach;
endif;
?>
<!-- required hidden field for admin-ajax.php -->
<input type="hidden" name="action" value="ccformationfilter" />
</form>
</div>
<?php
// ma boucle qui me ramène tous les custom post type "formation"
$paged = (get_query_var('paged')) ? get_query_var('paged') : 1;
$args=array(
'post_type' =>'formation',
'posts_per_page' =>5,
'paged' => $paged

);
$query = new WP_Query( $args ); ?>
<?php if ( $query->have_posts() ) : ?>
<div id="cc_formation_wrap" class="flex-row">
<?php while ( $query->have_posts() ) : $query->the_post();
$termsArray = get_the_terms( $post->ID, "categories-formations" ); // On récupère la taxonomy
$termsString = "";
foreach ( $termsArray as $term ) {
$termsString .= $term->slug.' ';
}
?>
<div class="<?php echo $termsString;?> flex-col-sm-6 flex-col-md-4 item">
<article id="post-<?php the_ID(); ?>" <?php post_class('related-formation-post');?>>
<div class="isotope-thumbnail">
<?php
if ( has_post_thumbnail() ) {
the_post_thumbnail('masonryLayout');
} ;
?>
</div>
<div class="isotope-title">
<?php
the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );
?>
</div>
<div class="entry-meta">
<?php if(get_field('duree')){
$duree = get_field_object('duree');
echo '<div><span class="isotope-duree">' . get_field('duree') . '</span></div>';
}
?>
</div>
<div class="isotope-excerpt">
<?php echo get_post_meta(get_the_ID(), '_yoast_wpseo_metadesc', true); ?>
</div>
<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Voir</a>
</div>
</article>
</div> <!-- end item -->

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

<?php

if ( $query->max_num_pages > 1 ) :
echo '<div class="loadmore_block"><div id="cc_formation_loadmore">Afficher plus de formations</div></div>'; // you can use <a> as well
endif;

else :
get_template_part( 'template-parts/content', 'none' );
endif;
?>
</div><!-- .flex-container -->

<?php
// hack : je replace ici the content(), pour laisser l'utilisateur administrer cette partie de la page

if ( have_posts() ) :
while ( have_posts() ) : the_post();
the_content();
endwhile;
endif;
;?>
</main><!-- #main -->
</div><!-- #primary -->
<?php // les variables qui vont servir l'ajax ?>
<script>
var posts_myajax = '<?php echo serialize( $query->query_vars ) ?>',
current_page_myajax = 1,
max_page_myajax = <?php echo $query->max_num_pages ?>
</script>
<script src="<?php bloginfo('template_url')?>/js/load-more.js"></script>
<?php get_footer();?>

Ici, il faut faire attention aux éléments suivants :

  • L’id du formulaire : #formation_filters
  • Le nom du bouton radio qui va nous permettre de récupérer la sélection de l’utilisateur : name= »category_formation_filters »
  • La valeur du champ caché pour l’envoi du formulaire : value= »ccformationfilter » qui devra correspondre au nom de mon action ajax pour les filtres.

Contrairement au précédent article, les variables servant wp_localize_script ne sont pas injectés via le fichier functions.php, mais directement à la fin de mon fichier page-formation.php. Ce sont ces variables qui seront utilisées dans le fichier js, donc attention à la nomenclature !

L’appel au fichier js qui va contenir tout le code est aussi appelé directement depuis cette page.

<script>
var posts_myajax = '<?php echo serialize( $query->query_vars ) ?>',
current_page_myajax = 1,
max_page_myajax = <?php echo $query->max_num_pages ?>
</script>
<script src="<?php bloginfo('template_url')?>/js/load-more.js"></script>

Ainsi, en changeant juste le nom des variables et en créant un fichier distinct, vous pourrez étendre ce système à tous vos custom post types !

Création du fichier formation-query.php

Dans un dossier à part, je crée un nouveau fichier php que j’appelle formation-query.php. Vous pouvez évidemment lui attribuer le nom de votre choix.

Dans le fichier functions.php, je l’inclue avec un require :

/**
* Formation query
*/

require get_template_directory() . '/inc/formation-query.php';

Ce fichier va contenir deux grandes fonctions et deux boucles, l’une pour les filtres, l’autre pour le bouton ajax load more. On reprend la même logique que pour les articles de blogs de mon précédent article, mais on sépare à présent les fichiers pour une arborescence plus propre, et un code plus facilement maintenable.

<?php 

add_action('wp_ajax_loadmoreformationbutton', 'cc_formation_loadmore_ajax_handler');
add_action('wp_ajax_nopriv_loadmoreformationbutton', 'cc_formation_loadmore_ajax_handler');

function cc_formation_loadmore_ajax_handler(){

$args = json_decode( stripslashes( $_POST['query'] ), true );
$args['paged'] = $_POST['page'] + 1;
$args['post_status'] = 'publish';
$args["post_type"] = 'formation';
query_posts( $args );
global $wp_query;
if( have_posts() ) :
while( have_posts() ): the_post();
$termsArray = get_the_terms($post->ID, "categories-formations");
$termsString ="";
foreach ( $termsArray as $term ) {
$termsString .= $term->slug;
}
?>
<div class="<?php echo $termsString;?> flex-col-sm-6 flex-col-md-4 item">
<article id="post-<?php the_ID(); ?>" <?php post_class('related-formation-post');?>>
<div class="isotope-thumbnail">
<?php
if ( has_post_thumbnail() ) {
the_post_thumbnail('masonryLayout');
} ;
?>
</div>
<div class="isotope-title">
<?php the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );?>
</div>
<div class="isotope-excerpt">
<?php echo get_post_meta(get_the_ID(), '_yoast_wpseo_metadesc', true); ?>
</div>
<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Voir</a>
</div>
</article>
</div> <!-- end item -->
<?php
endwhile;
endif;
die;
}

add_action('wp_ajax_ccformationfilter', 'cc_formation_filter_function');
add_action('wp_ajax_nopriv_ccformationfilter', 'cc_formation_filter_function');



function cc_formation_filter_function(){

if( isset( $_POST['all'] ) )
$terms = get_terms( array( 'taxonomy' => 'categories-formations' ) );
$args['tax_query'] = array(
array(

'taxonomy' => 'categories-formations',
'field' => 'term_id',
'terms' => $terms,
"posts_per_page" => 5
)
);





$args["post_type"] = 'formation';
$args['tax_query'] = array(
array(
'taxonomy' => 'categories-formations',
'field' => 'term_id',
'terms' => $_POST['category_formation_filters'],
"posts_per_page" => 5
)
);

$query = new WP_Query($args);
if( $query->have_posts() ) :
ob_start();
while( $query->have_posts() ): $query->the_post();

$termsArray = get_the_terms($post->ID, "categories-formations");
$termsString ="";
foreach ( $termsArray as $term ) {
$termsString .= $term->slug;
}
?>
<div class="<?php echo $termsString;?> flex-col-sm-6 flex-col-md-4 item">
<article id="post-<?php the_ID(); ?>" <?php post_class('related-formation-post');?>>
<div class="isotope-thumbnail">
<?php
if ( has_post_thumbnail() ) {
the_post_thumbnail('masonryLayout');
}
?>
</div>
<div class="isotope-title">
<?php the_title( '<h2 class="entry-title"><a href="' . esc_url( get_permalink() ) . '" rel="bookmark">', '</a></h2>' );?>
</div>
<div class="entry-meta">
<?php if(get_field('duree')){
$duree = get_field_object('duree');
echo '<div><span class="isotope-duree">' . get_field('duree') . '</span></div>';
}
?>
</div>
<div class="isotope-excerpt">
<?php echo get_post_meta(get_the_ID(), '_yoast_wpseo_metadesc', true); ?>
</div>
<div class="isotope-cta">
<a href="<?php esc_url( the_permalink() );?>" rel="bookmark">Voir</a>
</div>
</article>
</div> <!-- end item -->
<?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( $query->query_vars ),
'max_page' => $query->max_num_pages,
'found_posts' => $query->found_posts,
'content' => $posts_html
) );
die();
}

Le bouton ajax load more

Ici, mon action ajax s’appelle: loadmoreformationbutton

Dans ma fonction cc_formation_loadmore_ajax_handler, je crée la même boucle que dans mon modèle de page et custom post type page-formation.php.

Le sytème de filtre

Pour mes filtres, mon action ajax s’appelle : ccformationfilter, le même nom que la valeur de mon champs caché dans le formulaire de filtrage.

<input type="hidden" name="action" value="ccformationfilter" />

Je crée une première boucle pour mon bouton « all », sans distinction de catégorie:

$terms = get_terms( array( 'taxonomy' => 'categories-formations' ) );

Juste en dessous, je crée une deuxième boucle qui me ramène les articles de la catégorie sélectionnée par l’utilisateur

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

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

Le fichier js

Comme je l’ai indiqué plus haut, on doit faire appel à un nouveau fichier js que j’ai appelé load-more.js

A l’intérieur, on va créer nos deux fonctions, pour les filtres et le bouton load more.

jQuery(function($){

/* LOAD MORE FUNCTION ON FORMATION ARCHIVE PAGE */
$('#cc_formation_loadmore').click(function(){
data = {
'action': 'loadmoreformationbutton', // le nom de mon action ajax
'query': posts_myajax, // ma variable attribuée dans le fichier page-formation.php
'page' : current_page_myajax // ma variable attribuée dans le fichier page-formation.php
};
$.ajax({
url : '/monsite/wp-admin/admin-ajax.php', // AJAX handler
data : data,
type : 'POST',
beforeSend : function ( xhr ) {
$('#cc_formation_loadmore').text('Recherche...'); // ici l'ID de mon bouton load more
},
success : function( posts ){
if( posts ) {

$('#cc_formation_loadmore').text( 'Afficher plus de formations' );
$('#cc_formation_wrap').append( posts );
current_page_myajax++;

if ( current_page_myajax == max_page_myajax )
$('#cc_formation_loadmore').hide();

} else {
$('#cc_formation_loadmore').hide();
}
}
});
return false;
});

/* FILTERING FUNCTION ON FORMATION ARCHIVE PAGE */
$('#formation_filters').change(function(){
$.ajax({
url : '/numgrade/wp-admin/admin-ajax.php',
data : $('#formation_filters').serialize(), // form data
dataType : 'json',
type : 'POST',

success : function( data ){

current_page_myajax = 1;

posts_myajax = data.posts;

max_page_myajax = data.max_page;

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

if ( data.max_page < 2 ) {
$('#cc_formation_loadmore').hide();
} else {
$('#cc_formation_loadmore').show();
}
}
});

return false;

});

});

La liaison entre tous ces fichiers

Pour que le tout fonctionne, il faut surtout bien faire attention à nommer les éléments de la même manière d’une page à l’autre.

Les différents ID et nom attribués dans les autres fichiers se retrouvent dans le js:

L’ID du conteneur principal : #cc_formation_wrap

L’ID de mon bouton load more : #cc_formation_loadmore

Le nom de mon action ajax : loadmoreformationbutton

L’ID de mon formulaire de filtrage : #formation_filters

mes variables crées dans le fichier page-formation.php  : current_page_myajax , posts_myajax et max_page_myajax.

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.