Meta box : créez une liste de tâches

Aujourd’hui, pour continuer la série des meta box, je vous propose de découvrir comment réaliser une “To Do List” dynamique.

Cette metabox contient un nombre indéfini de champs textes, qui peuvent être modulés par le biais de boutons d’ajout/suppression de ligne.

Je vais vous montrer comment la créer, pas à pas, et vous expliquer son fonctionnement. Il risque d’y avoir des bouts de php/js partout mais rassurez-vous, vous trouverez le code complet en fin d’article.

meta-box-liste-dynamique
Une meta box pour vos “To Do List”

Initialisation de la metabox « liste de tâches »

Comme lorsque l’on créer n’importe quelle metabox, il faut toujours commencer par l’initialiser. Cette étape sert à donner un nom à notre meta box, et à lui préciser où et sur quel type de contenu elle doit s’afficher.

Rien de bien sorcier donc…

add_action( 'add_meta_boxes', 'mes_metaboxes' );
function mes_metaboxes() {
add_meta_box( 'to_do_list', 'choses à faire', 'to_do_list', 'post', 'normal', 'default' );
}

Structure de base

Pour la fonction de construction de base, nous allons simplement :

  1. Récupérer des tâche potentiellement enregistrées
  2. Faire une boucle et afficher un champ pour chaque entrée existante, avec “descr_chose[]” en guise d’attribut name
  3. Afficher un champ vide, seul, si aucune tâche n’est enregistrée

Pour l’instant la metabox ne peut enregistrer qu’une seule entrée, donc forcément le foreach ne sert à rien… mais nous allons vite en avoir besoin.

Pour la sauvegarde, on prépare une simple boucle qui, pour chaque input[name= »descr_choses »], ajoutera une valeur à la meta_key “_descr_chose”.

add_action('save_post','ma_sauvegarde');
function ma_sauvegarde($post_id){
if((!defined( 'DOING_AJAX' ) || !DOING_AJAX ) && isset($_POST['descr_chose'])){
// je supprime les anciennes valeur...
delete_post_meta($post_id, '_descr_chose');

//...et j'enregistre toutes les nouvelles
foreach($_POST['descr_chose'] as $d){
//si elles ne sont pas vides
if(!empty($d))
add_post_meta($post_id, '_descr_chose', $d);
}
}
}

Nonce upon a time in WordPress…

Aurais-je omis de vous parler des nonces ? Oui en fait j’avoue… mais je vais réparer mon erreur ^^

Les nonces (number used once) sont des outils qui permettent de protéger notre CMS des failles de sécurité type CSRF. En gros, dans chaque metabox nous allons rajouter un nombre généré aléatoirement, dans un champ caché. Avant la sauvegarde des données de la metabox, la valeur du champ sera comparée à la valeur attendue.

Si elle diffère, on ne sauvegardera rien car cela voudra dire que quelqu’un a profité du fait que l’on soit connecté à WordPress pour nous faire mettre à jour des données contre notre gré.

WordPress propose des fonctions toutes faites pour ça. Ici on va utiliser wp_nonce_field() pour créer le nonce, et check_admin_referer() pour tester sa valeur avant de sauvegarder.

Ainsi, dans notre fonction constructrice, en dehors de la div “#all_things” on ajoute :

// nonce
wp_nonce_field('update-taches_'.$post->ID,'_wpnonce_update_taches');

.. et dans la fonction de sauvegarde, après la condition j’ajoute :

// check referer
check_admin_referer( 'update-taches_'.$post_id,'_wpnonce_update_taches' );

Dynamisons tout ça!

Actuellement notre metabox n’est pas du tout opérationnelle. Nous ne pouvons pas ajouter ni supprimer de tâches.

Pour rendre cela possible nous allons rajouter quelques boutons et faire un peu de javascript.

En premier lieu nous avons besoin d’un bouton pour ajouter un élément. Il ne s’affichera que si le javascript est activé. Copiez le code ci-dessous juste après la <div id= »all_things »></div>.

Ensuite il nous faut un bouton pour supprimer les éléments, ci-besoin. Rajouter ce code avant la fermeture des <div> ayant la classe “item-chose” :

<a class="suppr-chose button-secondary hide-if-no-js" href="javascript:void(0);">supprimer</a>
<span class="hide-if-js"><em>Pour supprimer, videz les contenus.</em></span>

Nous allons maintenant pouvoir faire interagir ces boutons avec du javascript.

Un javascript pour ajouter/supprimer des champs

Toujours dans le code de construction de la meta box, en dessous du reste, ajouter une balise <script> avec le code suivant :

// jQuery no-conflict, car on est dans l'admin
jQuery(document).ready(function($){
//je créer une fonction
function remove_chose(){
// lorsque l'on clic sur le bouton de suppression,
// son parent est supprimé
$('.suppr-chose').on('click',function(){
$(this).parent().remove();
});
}
// je lance cette fonction
remove_chose();

// lorsque l'on clique sur "ajouter une tâche"...
$('#ajout-chose').on('click',function(){
// ... on duplique/vide qui va bien, à la suite...
$('.item-chose:last').clone().appendTo('#all_things');
$('.item-chose:last input').val('');
// ... et on relance la fonction remove_chose
remove_chose();
});
});

Vous avez remarqué que l’on relance la fonction remove_chose() après l’ajout d’un nouvel élément de formulaire ?
C’est parce que l’on veut qu’il puisse être supprimé. Il faut donc lui attacher le même écouteur d’événement qu’aux champs déjà existants.

Le problème de get post meta

On en avait déjà parlé dans l’article de découverte des metaboxes, des fois la fonction get_post_meta() n’est pas toujours adaptée pour récupérer des metadonnées. Ici par exemple, si on essaie la meta box, nous allons constater que les tâches sont bien enregistrées, mais qu’elles ressortent dans n’importe quel ordre… C’est un peu problématique.

Et encore là ça va, mais attendez que l’on veuille ajouter des informations complémentaires à chaque tâche. Ça risque de faire n’importe quoi…

La solution consiste à créer une fonction que l’on utilisera à la place de get_post_meta() et qui ressortira les mêmes infos, mais ordonnées par “meta_id”.

function get_post_meta_ordered($id,$meta_key){
global $wpdb;
$output = array();
$sql = "SELECT m.meta_value FROM ".$wpdb->postmeta." m where m.meta_key = '".$meta_key."' and m.post_id = '".$id."' order by m.meta_id";
$results = $wpdb->get_results( $sql );
foreach( $results as $result ){
$output[] = $result->meta_value;
}
return array_filter($output);
}

Il faut coller le code ci-dessus dans votre fonctions.php, et ensuite remplacer , dans le code de notre meta box :


$to_do = get_post_meta($post->ID,'_descr_chose',false);

.. par :

// devient
$to_do = get_post_meta_ordered($post->ID,'_descr_chose');

Maintenant ça fonctionne bien, les tâches ressortent dans le bon ordre.

On passe à la suite…

Ajout d’un deuxième champ pour la date

Maintenant que nous pouvons ajouter et modifier des tâches de la liste, il serait bien de pouvoir y associer une durée.

Ce n’est pas très complexe. Il suffit d’ajouter, à l’intérieur de chaque div ayant la classe “item_chose” (avant le bouton « supprimer »), un second champ de type texte :

<label for="">Durée allouée : </label><input id="" class="duree_des_choses" style="width: 56px;" type="text" name="duree_chose[]" />h

Il faut penser à ajouter cet élément à trois endroits :

  1. Dans la boucle qui affiche les données sauvegardées
  2. À la suite de ce qui est affiché si aucune donnée n’est présente

On récupère et on sauvegarde les durées de la même manière que les tâches.
Il faut juste, dans le foreach de to_do_list(), utiliser la clef de l’élément en cours pour afficher la bonne durée.

$duree_chose = get_post_meta_ordered($post->ID,'_duree_chose');
$to_do = get_post_meta_ordered($post->ID,'_duree_chose');
// je test s'il y a autant de tâches que de durées
if(count( $to_do )>0 && count( $to_do )==count( $duree_chose ) ):
foreach($to_do as $k => $thing){
?>
<div class="item-chose"><label for="">Description : </label><input id="" class="description_des_choses" style="width: 50%;" type="text" name="descr_chose[]" value="<?php echo $thing; ?>" />
<label for="">Durée allouée : </label><input id="" class="duree_des_choses" style="width: 56px;" type="text" name="duree_chose[]" value="<?php echo $duree_chose[$k]; ?>" />h
<a class="suppr-chose button-secondary hide-if-no-js" href="javascript:void(0);">supprimer</a>
<span class="hide-if-js"><em>Pour supprimer, videz les contenus.</em></span></div>
<?php
}
//...

Vu que le tableau des dates et celui des tâches ont systématiquement autant d’éléments, et qu’ils sortent dans le même ordre, ils correspondront à coup sûr.

Obtenir la durée totale

Si vous souhaitez obtenir la durée de toutes les tâches confondues, il suffit de faire :

$duree_totale = array_sum( get_post_meta( $post->ID, '_duree_chose', false ) );
echo $duree_totale;

Cela peut servir autant en front office que dans l’admin.

Récapitulons…

Au fil de cet article on a construit la meta box petit bout par petit bout. Il est temps de revoir son code en entier :

//Initialisation
add_action('add_meta_boxes','mes_metaboxes');
function mes_metaboxes(){
add_meta_box('things', 'choses à faire', 'things_to_do', 'post', 'normal', 'default');
}

//fonction alternative à get_post_meta
function get_post_meta_ordered($id,$meta_key){
global $wpdb;
$output = array();
$sql = "SELECT m.meta_value FROM ".$wpdb->postmeta." m where m.meta_key = '".$meta_key."' and m.post_id = '".$id."' order by m.meta_id";
$results = $wpdb->get_results( $sql );
foreach( $results as $result ){
$output[] = $result->meta_value;
}
return array_filter($output);
}

// Fonction de construction de la metabox
function things_to_do($post){
global $wpdb;

//taches
$to_do = get_post_meta_ordered($post->ID,'_descr_chose');
//duree
$duree_chose = get_post_meta_ordered($post->ID,'_duree_chose');

// nonce
wp_nonce_field( 'update-taches_'.$post->ID, '_wpnonce_update_taches' );

//boucle
echo '<div id="all_things">';
if(count( $to_do )>0 && count( $to_do )==count( $duree_chose ) ):
foreach($to_do as $k => $thing){
$duree_c = $duree_chose[$k];
?>
<div class="item-chose"><label for="">Description : </label><input id="" class="description_des_choses" style="width: 50%;" type="text" name="descr_chose[]" value="<?php echo $thing; ?>" /> <label for="">Durée allouée : </label><input id="" class="duree_des_choses" style="width: 56px;" type="text" name="duree_chose[]" value="<?php echo $duree_c; ?>" />h <a class="suppr-chose button-secondary hide-if-no-js" href="javascript:void(0);">supprimer</a></div>
<span class="hide-if-js"><em>Pour supprimer, videz les contenus.</em></span>

<?php
}
endif; ?>

<div class="item-chose"><label for="">Description : </label><input id="" class="description_des_choses" style="width: 50%;" type="text" name="descr_chose[]" /> <label for="">Durée allouée : </label><input id="" class="duree_des_choses" style="width: 56px;" type="text" name="duree_chose[]" />h <a class="suppr-chose button-secondary hide-if-no-js" href="javascript:void(0);">supprimer</a></div>
<span class="hide-if-js"><em>Pour supprimer, videz les contenus.</em></span>
<?php
echo '</div>';
?>

<!-- lien ajout -->

<a id="ajout-chose" class="button-primary hide-if-no-js" style="margin-top: 10px; position: relative; display: inline-block;" href="javascript:void(0);">Ajouter une tâche</a>
<span class="hide-if-js">Pour ajouter une nouvelle entrée, sauvegardez l'article.
<em>JAVASCRIPT désactivé, vous aurez une meilleure ergonomie avec le javascript activé.</em></span>

<!-- script-->
<script type="mce-text/javascript">// <![CDATA[
jQuery(document).ready(function($){
//suppresion champ
function remove_chose(){
$('.suppr-chose').on('click',function(){
$(this).parent().remove();
});
}
remove_chose();

//ajout champ
$('#ajout-chose').on('click',function(){
$('.item-chose:last').clone().appendTo('#all_things');
$('.item-chose:last input').val('');
remove_chose();
});
});

// ]]></script>
<?php
}

// Sauvegarde
add_action('save_post','ma_sauvegarde');
function ma_sauvegarde($post_id){

if( ( !defined( 'DOING_AJAX' ) || !DOING_AJAX ) && isset( $_POST['descr_chose'], $_POST['duree_chose']) ){
check_admin_referer( 'update-taches_'.$post_id,'_wpnonce_update_taches' );
delete_post_meta($post_id, '_descr_chose');
delete_post_meta($post_id, '_duree_chose');
foreach($_POST['descr_chose'] as $d){
if(!empty($d))
add_post_meta($post_id, '_descr_chose', $d);
}
foreach($_POST['duree_chose'] as $d){
if(!empty($d))
add_post_meta($post_id, '_duree_chose', $d);
}
}
}

Quelques exemples d’utilisation

Cette metabox est une de celle que j’utilise le plus souvent.
Elle convient parfaitement pour :

  • Associer une “to do list” à un type de contenu
  • Enregistrer des données de type inventaire
  • Spécifier les caractéristiques d’un produit en vente

Nous l’avons travaillé ici avec seulement deux champs (date + description) mais rien ne nous empêche d’en ajouter davantage.

Et vous, vous en ferez quoi ?

5 commentaires
Ha une suite, bien sympa encore une fois ! Je vais devoir créer un dossier wabeo moi.

Je me permet de faire quelques corrections :
1) Erreur d’affichage de la description :
Ligne 635 :
foreach($to_do as $k => $thing){
au lieu de
foreach($to_do as $k => $t){
puisque tu affiches « $t » dessous.

2) Optimisation code :
Ligne 612 :
$results = $wpdb->get_results( $sql );
foreach( $results as $result )
{
$output[] = $result->meta_value;
}
return $output;
au lieu de
return $wpdb->get_col( $sql );
En effet, $wpdb->get_col() fait ce que tu souhaites en sortie et récupère bien une colonne de résultats.

3) Opti encore
Ligne 678 :
foreach($_POST[‘descr_chose’] as $d){
add_post_meta($post_id, ‘_descr_chose’, $d);
}
au lieu de
add_post_meta($post_id, ‘_descr_chose’, $_POST[‘descr_chose’]);
Oui, pourquoi découper en morceau si c’est quand même pour tout récupérer en en plus se casser la tête à faire une requête maison pour mettre tout ça en ordre !

4) Nonce field
Ton nonce field est correct, mais un vrai bon nonce field se décompose comme ceci :
« verbe-nom_quelID »
ce qui donne pour toi
wp_nonce_field(‘update-taches_’.$post->ID, ‘_wpnonce_update_taches’);
au lieu de
wp_nonce_field(‘update_taches’, _wpnonce_update_taches’);
Sinon ton nonce sera unique pour tous tes posts. Aussi le fait de découper avec – puis ensuite _ est utile si on souhaite expliquer le nonce en cas d’erreur de vérification, par défaut on a « Etes vous sur de vouloir faire cela ? » Mais cela quoi ?? On peut donc utiliser wp_explain_nonce() avec le filtre ‘explain_nonce_’ . $verb . ‘-‘ . $noun
Et dans le même point j’ajoute que le check_admin_referer() n’est pas obligé de se trouver dans le IF, je l’ai donc volontairement mis sous le IF pour le montrer, la fonctione ne retourne pas false, mais un die, donc bon …

5) Pas de noJS ?
Dommage ! Mon client est aveugle et utilise un screen reader, il ne peut pas interagir avec ces box. (oui les aveugles ont internet et surfent …)

6) jQuery sans live
« Live » c’est la vie ! ^^

Last) Voici mon code complet retouché:
1) j’y ai ajouté une gestion du no JS en affichant/cachant certains éléments, en no JS, pour ajouter un 2eme element, il faut sauvegarder entre 2 (oui c’est moins bien, mais c’est no JS !)

2) J’ai modifié le jQuery pour supprimer la fonction remove_chose() et jouer avec un live et aussi un clone.

3) La fonction ma_sauvegarde a donc été modifiée aussi concernant le nonce, le check admin ref et la sauvegarde des données directement en array.

4) Bref un peu tout, je te laisse me poser des questions sur ce que tu comprends pas dans mes modifs au cas où.

Code sous pastebin : http://pastebin.com/iXscWrRF

Willy Bahuaud, il y a 12 ans
Youhouh !!

Tu gères !

Faut que je regarde ça. Y’a deux trois point où je doute que ce soit mieux par contre :

– concernant la fonction live de jQuery, je suis pas sûr mais il me semble qu’elle est juste super gourmande (d’où ma pirouette…)
– Il y a une différence entre pousser un tableau dans une meta et ajouter un tas de meta_value différentes dans autant de meta_key. Si j’ai fait comme ça, c’est que je trouve ça plus simple pour intéragir avec par la suite (pour des meta_query, par exemple…) mais forcément j’ai pas abordé ça ici :-/

Merci pour le reste je connaissait pas 🙂 , et merci de m’avoir repris sur les nonces (je découvre ce truc et ça reste un peu flou encore)

Je mets à jour l’article rapidement…

Alors oui tu as raison sur les meta_query tout à fait, et comme tu le dis, ne l’abordant pas (et même, comment l’aborder avec une todo list !?) je n’y avais pas pensé.
Donc ai-je bien fait, je ne sais pas …
Pour le .live() si c’est trop gourmand, mea culpa, ne touche pas ton code, quant à mon .clone() ça te plait ? 😉
Je retouche le code, j’ai fait une tite bourde qui crée un Warning.
Voilà le dernier code sans le warning : http://pastebin.com/tGufZ8sh
Willy Bahuaud, il y a 12 ans
Oui, ton clone est une excellente idée !

Je pense mettre à jour l’article ce soir avec tes idées, ça sera bien mieux 🙂

Merci pour le code !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Publié le 11 juin 2012
par Willy Bahuaud
Catégorie Développement WordPress