Meta box : reliez des contenus entre eux

Pour illustrer la metabox du jour, le mieux est de se mettre en situation…

Imaginons que nous réalisons un site WordPress pour un établissement qui organise des colloques et des conférences. Sur ce site, mon client souhaiterai y présenter les évènements, en détail, ainsi que les intervenants.

On a plein de données à renseigner pour l’un comme pour l’autre. Nous allons donc créer deux types de contenus.

Mais bien sûr il va falloir les lier, pour dire quel conférencier participe à quel colloque. Il nous faut une metabox !

Problème : Il y a plusieurs conférenciers pour chaque évènement, je ne peut donc pas utiliser un champ select pour faire la liaison. Je ne peux pas non plus utiliser des cases à cocher car j’ai environ 150 intervenants dans ma base, ça va faire un paquet de checkbox…

L’idéal serait une metabox avec un champ de recherche pour trouver les conférenciers. Lorsque l’on trouverait celui qui nous intéresse nous pourrions l’ajouter à une liste qui sera sauvegardée dans une metadonnée du colloque.

C’est ce que nous allons faire…

meta-box-autocompletion-wordpress-admin
Une metabox pour faire des liaisonsentre contenus

On va avoir besoin de ça

Avant de se jeter dans le code, commençons par faire l’inventaire de ce dont nous allons avoir besoin.

  1. Comme toujours, il faudra commencer par les fonctions habituelles d’initilisation/construction/sauvegarde de la meta box
  2. Pour le champ de recherche, un système d’autocomplétion serait parfait. On commencerait à saisir le nom d’un conférencier qu’il apparaitrait dans le champ. Un clic dessus nous permettrai de l’ajouter à une liste ainsi qu’a un champ caché. Pour le système d’autocomplétion, je propose jquery-ui-autocomplete.
  3. Il nous faut donc aussi un tableau javascript de tous les intervenants potentiels…
  4. … et un champ caché
  5. … et une liste non-ordonnée, en HTML, pour faire un retour visuel à l’utilisateur
  6. Qui dit HTML dit aussi un peu de css pour la mise en forme
  7. Pour finir bah il reste le gros javascript de bœuf. On va s’inspirer de celui-ci pour le faire évoluer selon nos besoins.

Le code PHP

Notre metabox va reposer à 100% sur jquery autocomplete, il faut donc l’ajouter à l’admin WordPress, sans oublier les styles qui vont bien.

Le code suivant est à ajouter au fichier functions.php (vous trouverez le style ici). Pensez à mettre à jour les chemins…

add_action( 'admin_enqueue_scripts', 'my_admin_scripts_method' );
function my_admin_scripts_method() {
  if( is_admin() ){
    wp_enqueue_script( 'jquery-ui-autocomplete' );
    wp_register_style( 'jquery.ui.theme', get_bloginfo( 'template_url' ) . '/css/jquery-ui-1.8.19.custom.css');
    wp_enqueue_style( 'jquery.ui.theme' );
  }
}

Maintenant qu’on a mis en place la librairie, nous pouvons attaquer la metabox.

J’initialise…

// Initialisation de la metabox, pour le CPT "conference"
add_action('add_meta_boxes','mes_metaboxes');
function mes_metaboxes(){
  add_meta_box('conferenciers_presents', 'Conférenciers présents', 'conferenciers_concernes', 'conference', 'side', 'default');
}

… je construis…

//Construction
function conferenciers_concernes($post){
//je récupère la meta potentiellement sauvegardée
$conferenciers_presents = get_post_meta($post->ID,'_conferenciers_presents',false);

//je créer un nonce
wp_nonce_field('update-conferenciers_'.$post->ID, '_wpnonce_update_conferenciers);

//mon widget
echo '<div class="ui-widget">';
// le champ qui servira de support à autocomplete
echo '<label for="nom">Nom : </label><input id="nom" type="text" />';
// la liste des conférenciers concernés (assurant un retour visuel pour l'utilisateur)
echo '<ul>';
// j'y affiche toutes les entrées déjà sauvegardées dans la meta</ul>
if( ! empty( $conferenciers_presents) )
  foreach( $conferenciers_presents as $c )
    echo '<li data-id="' . $c . '"><span class="erase">x</span> ' . get_the_title($c) . '</li>
echo '<ul>';

// mon champ caché, que je mettrai à jour et sauvegarderai
// il contient déjà les valeurs de la meta
echo'<input id="conf_presents" type="hidden" name="conf_presents" value="'.implode(',',$conferenciers_presents).'" />';

//fin du widget
echo '</div>';

… et je sauvegarde.

//sauvegarde
add_action('save_post','sauvegarde_metabox');
function sauvegarde_metabox($post_id){
    //s'il s'agit bien d'une sauvegarde volontaire
    if( ( !defined( 'DOING_AJAX' ) || !DOING_AJAX ) && isset($_POST['conf_presents'])){
        //test du nonce
        check_admin_referer( 'update-conferenciers_'.$post_id,'_wpnonce_update_conferenciers' );

        // je suprrime tout
        delete_post_meta($post_id,"_conferenciers_presents");
        //j'éclate mon input
        $conf = explode(',',$_POST['conf_presents']);
        foreach($conf as $c){
            //pour chaque entrée j'ajoute une meta
            add_post_meta($post_id, "_conferenciers_presents", intval($c));
        }
    }
}

Pour l’instant notre meta box ne fait rien du tout. Pour cause on vient juste d’en poser les fondations ; rien en sera actif sans le javascript.

3 tonnes de javascript

On arrive à la grosse partie. Avant toute chose, autocomplete va avoir besoin d’une liste des éléments qui peuvent être appelés. Ce qu’on veut c’est lister tous les intervenants dans la base.

Il faut donc créer un tableau en js, et le remplir via une boucle WordPress renvoyant la totalité des conférencier.

Créer une balise <script> dans la metabox, et ajouter ici ce code :

//no-conflict
jQuery(function($) {

  // un tableau avec tous les conférenciers que l'on peut sélectionner
  var availableTags = <?php $confe = get_posts('post_type=conferencier&posts_per_page=-1');
  foreach($confe as $cf){
    echo '{value:"'.$cf->ID.'",label:"'.esc_js($cf->post_title).'"},'."\n";
  }?>
});

On a maintenant tout ce qu’il nous faut pour coder l’ajout d’un intervenant, à savoir : un tableau qui donne pour chaque conférencier son nom et son ID

L’ajout d’un intervenant

Toujours dans la même balise <script>, on conçoit la fonction d’autocomplétion.

//autocomplete sur le champ #nom
$( "#nom" ).autocomplete({
  // je mets le tableau précédemment crée en source
  source: availableTags,
  // lorsqe l'on sélectionne un élément
  select: function(event,ui){
    //je crée un nouveau
    <ul>
      <li>var li = '</li>
      <li data-id="' + ui.item.value + '"><span class="erase">x</span> ' + ui.item.label + '</li>
      <li>';
    //je fais un tableaux des conférencier déjà ajouté
    var all_conf_presents = new Array();
    all_conf_presents =($('#conf_presents').val()!='') ? $('#conf_presents').val().split(',') : [];
    // si il est déjà dans la liste, j'en tiens pas compte
    if($.inArray(ui.item.value,all_conf_presents)!="-1"){
      $(this).val('');
    }else{
      //sinon je l'ajoute à la liste
      all_conf_presents.push(ui.item.value);
      //je pousse cette liste dans le champ caché
      $('#conf_presents').val(all_conf_presents);
      //et j'ajoute la nouvelle entrée dans le <ul>
      var $cp= $( "#conferenciers_presents" );
      $cp.append(li);
      $(this).val('');</ul>
    }
    //juste pour que la sélection d'un élément ne remplisse pas le input (comportement normal)
    return false;
  }
});

Voilà, dès maintenant la meta box nous permet d’ajouter des entrées. Pas de problème avec la sauvegarde non plus. Par contre il n’est toujours pas possible de supprimer un élément.

Suppression d’un conférencier

Encore et toujours dans la même balise <script>, à la suite du reste, on ajoute le code ci-dessous qui va nous permettre de retirer/dissocier un intervenant de l’événement.

L’écouteur d’événement est contenu dans fonction, de façon à pouvoir le re-associer facilement aux éléments qui seront ajoutés par la suite (j’aime pas la fonction “live” de jQuery et j’assume…).

//function qui me sert à supprimer l'ID d'un conférencier dans #conf_presents
function removeByElement(arrayName,arrayElement){
  for(var i=0; i if(arrayName[i]==arrayElement)
    arrayName.splice(i,1);
  }
}

//évènement de suppression de conférencier
function listenerremove(){
  $( "#conferenciers_presents" ).find('li .erase').on('click',function(){
    // suppression élément
    var $elem = $(this).parent('li'); //je cible l'élément à supprimer
    //je construit un talbeau avec les conférencier actuellement liés
    var all_conf_presents = new Array();
    all_conf_presents =$('#conf_presents').val().split(',');
    //je récupère l'ID à retirer
    var dataval = $elem.attr('data-id');
    // je supprime l'ID du tableau
    removeByElement(all_conf_presents,dataval);
    //je supprime le conférencier dans la liste
    $elem.remove();
    //je supprime son ID dans le champ caché
    $('#conf_presents').val(all_conf_presents);
  });
}

//je lance la fonction
listenerremove();

Il ne faut pas oublier d’ajouter “listenerremove();” dans la fonction d’ajout, juste avant la fermeture du else :

var $cp= $( "#conferenciers_presents" );
$cp.append(li);
//ici
listenerremove();
}

Il ne nous reste plus qu’à ajouter un peu de CSS pour style le bouton de suppression. Dans la fonction de construction de la meta box, ajouter une balise <style>, et collez-y ces styles :

.erase{
    background:#2e2e2e;
    color:#FFF;
    padding:0 4px;
    -moz-border-radius:10px;
    -webkit-border-radius:10px;
    -o-border-radius:10px;
    border-radius:10px;
}
.erase:hover{
    background:#F20;
    cursor:pointer;
}

Le code dans son intégralité

Vous devriez maintenant obtenir ce résultat, dans votre fichier “functions.php” :


// chargement des scripts
add_action('admin_enqueue_scripts', 'my_admin_scripts_method');
function my_admin_scripts_method(){
  if(is_admin()){
    wp_enqueue_script('jquery-ui-autocomplete');
    wp_register_style('jquery.ui.theme', get_bloginfo('template_url') . '/css/jquery-ui-1.8.19.custom.css');
    wp_enqueue_style('jquery.ui.theme');
  }
}

// Initialisation de la metabox, pour le CPT "conference"
add_action('add_meta_boxes','mes_metaboxes');
function mes_metaboxes(){
  add_meta_box('conferenciers_presents', 'Conférenciers présents', 'conferenciers_concernes', 'conference', 'side', 'default');
}

//Construction
function conferenciers_concernes($post){
  $conferenciers_presents = get_post_meta($post->ID,'_conferenciers_presents',false);
  
  wp_nonce_field('update-conferenciers_'.$post->ID, '_wpnonce_update_conferenciers');

echo '<div class="ui-widget">';
echo '<label for="nom">Nom : </label><input id="nom" type="text" />';
echo '</div><ul>';
if(!empty( $conferenciers_presents)){
  foreach($conferenciers_presents as $c){
    echo '<li data-id="' . $c . '"><span class="erase">x</span> ' . get_the_title($c) . '</li>';
  }
}
echo '</ul>';

echo'<input id="conf_presents" type="hidden" name="conf_presents" value="'.implode(',',$conferenciers_presents).'" />';
echo '</div>';

?>
<script type="text/javascript">// <![CDATA[
jQuery(function($) {

    // un tableau avec tous les conférenciers que l'on peut sélectionner
    var availableTags = [<?php
    $confe = get_posts('post_type=conferencier&posts_per_page=-1');
    foreach($confe  as $cf){
      echo '{value:"'.$cf->ID.'",label:"'.esc_js($cf->post_title).'"},'."\n";
    }
    ?>];

    //autocomplete sur le champ #nom
  $("#nom").autocomplete({
    source: availableTags,
    select: function(event,ui){
      var li = '<li data-id="' + ui.item.value + '"><span class="erase">x</span> ' + ui.item.label + '</li>';
      var all_conf_presents = new Array();
      all_conf_presents =($('#conf_presents').val()!='') ? $('#conf_presents').val().split(',') : [];
      if($.inArray(ui.item.value,all_conf_presents)!="-1"){
        $(this).val('');
      }else{
        all_conf_presents.push(ui.item.value);
        $('#conf_presents').val(all_conf_presents);
        var $cp= $( "#conferenciers_presents" );
        $cp.append(li);
        $(this).val('');
        listenerremove();
      }         

      return false;
    }
  });

  //function qui me sert à supprimer l'ID d'un conférencier dans #conf_presents
  function removeByElement(arrayName,arrayElement){
    for(var i=0; i<arrayName.length;i++ ){ 
    if(arrayName[i]==arrayElement)
      arrayName.splice(i,1); 
    } 
  }

  //évènement de suppression de conférencier
  function listenerremove(){
    $("#conferenciers_presents").find('li .erase').on('click',function(){
      var $elem = $(this).parent('li');
      var all_conf_presents = new Array(); 
      all_conf_presents =$('#conf_presents').val().split(',');
      var dataval = $elem.attr('data-id');
      removeByElement(all_conf_presents,dataval);
      $elem.remove();
      $('#conf_presents').val(all_conf_presents);
    });
  }

  listenerremove();
 });

// ]]></script>

<?php }

//sauvegarde
add_action('save_post','sauvegarde_metabox');
function sauvegarde_metabox($post_id){
  if( ( !defined( 'DOING_AJAX' ) || !DOING_AJAX ) && isset($_POST['conf_presents'])){

    check_admin_referer( 'update-conferenciers_'.$post_id,'_wpnonce_update_conferenciers' );

    delete_post_meta($post_id,"_conferenciers_presents");
    $conf = explode(',',$_POST['conf_presents']);
    foreach($conf as $c){
      add_post_meta($post_id, "_conferenciers_presents", intval($c));
    }
  }
}

Utilisation de cette meta box

Notre meta box de liaison inter-contenus est opérationnelle à 100%. Grâce à elle on pourra faire ressortir les conférenciers associé à chaque colloque, lorsque l’on sera sur la page de la conférence, grâce à la fonction get_post_meta().

Nous pourrons aussi, sur la page du conférencier, lister ces interventions, via une requête personnalisée du style :

[git:precode_php@https://github.com/willybahuaud/related-posts-wordpress/blob/master/exemple-of-use.php]

Si vous avez des suggestions pour améliorer cette metabox, n’hésitez pas à me les faire connaître.

Bon usage ;-)

Contacter l'auteur :

willy bahuaud

Je suis Willy Bahuaud, intégrateur et développeur, spécialiste de WordPress.
Besoin de mes services ? Écrivez-moi !

15 commentaires

  1. Par Rahe — Il y a 5 années
    Pas mal, mais le plus simple est d’utiliser un plugin éprouvé de type post 2 posts : http://wordpress.org/extend/plugins/posts-to-posts/ codé par Nacin et gère beaucoup de choses comme la cardinalité des liaisons, la possibilité de faire directement les requêtes dans la WP_Query, des métas etc etc.. Le mieux est d’aller voir dans la documentation https://github.com/scribu/wp-posts-to-posts/wiki qi a une partie pour les développeurs. Sachant qu’il est bien écrit et niveau performances c’est assez puissant. On peut même taper dans son API et ses méthodes de classe . Ca permet même de lier un post type avec des users WordPress ;)
  2. Par Willy Bahuaud — Il y a 5 années
    Ouai il à l’air pas mal du tout ce petit plugin ! Merci pour sa présentation

    Après l’article est plus dans l’esprit “Do it Yourself” C’est pas que j’aime pas les plugins (bien au contraire ^^) mais je n’ai pas toujours tout le temps besoin de toutes les fonctionnalités du truc…

    Sinon c’est sympa aussi de voir comment ça marche ;-)

    Mais t’as raison, pour des utilisation avancés, ton plugin fera gagner du temps !

  3. Par Rahe — Il y a 5 années
    Oui oui parfois les différentes fonctionnalités ne sont pas toutes nécessaire, par contre ce plugin est très simple et ne fait pas dans les fioritures folles, on peut faire une liaison et la caractériser avec des métas.
    Je suis totalement d’accord que parfois le développement perso est largement suffisant ;), surtout si on veut packager notre fonctionnalité avec le thème :).
    Dans l’ensemble il vaut mieux utiliser un plugin de ce type qui est codé de façon magistrale et surtout par un core dev de WordPress :). La richesse de son wiki et de la doc montre qu’il y a une
    mise à jour de façon récurrente. J4ai posté un problème sur le github et j’ai eu une réponse dans la journée 🙂
  4. Par Julio Potier (BoiteAWeb) — Il y a 5 années
    Hello
    Je suis de l’avis de Raherian pour une véritable utilisation MAIS j’adore encore plus fouioller le code et réussir à faire ça à la main, avec2 bouts de ficelle (bon faut que ça reste propre et un minimum opti).
    Encore bravo, tu as dû passer un sacré bout de temps pour tous ces tutos meta box (c’est pas fini peut etre ?? :p)
    Petit détail (que tu avais aussi fait dans ton précédent tuto) c’est que tu testes « if($conferenciers_presents[0]!=NULL) » mais si « $conferenciers_presents[0] » n’existe pas du tout (il peut exister et valoir null) alors une notice apparait ! Il te faut tester avec isset() : « if( isset( $conferenciers_presents[0] ) ) » ou avec count() « if( count( $conferenciers_presents ) > 0 ) ».
    Je vais essayer de reproduire le tuto car il me semble que WP intègre un système qu’on peut détourner pour faire la recherche d’un post en ajax au lieu de tout intégrer mes 15000 conférienciers :p, il l’utilise dans la page des médias.
    Je te trouver ça ;)
  5. Par Rahe — Il y a 5 années
    @julio-potier-boiteaweb : oui i lfaut regarder du côté du fichier wp-admin\js\nav-menu.dev.js. Je l’ai utilisé pour la recherche avec relation post types, c’est dans la fichier js du plugin ;). C’est assez simple et bien fait 🙂
  6. Par Julio Potier (BoiteAWeb) — Il y a 5 années
    Non je pensais à $.findPosts() dans media.dev.js couplé avec find_posts_div() de WP couplé encore à du ob_start() avec callback pour replace du contenu.
    C’est un poil plus complexe, mais à l’arrivée je ne fais aucune requête de tous mes posts pour les mettre dans un ui-autocomplete, à la place, je fais des requêtes ajax selon le texte frappé, bien plus léger donc je pense.
    Non ?
    Je dois faire une démo ??
  7. Par Julio Potier (BoiteAWeb) — Il y a 5 années
    Je viens de tester en vrai et ça ne fonctionne pas, on dirait que tu modifies ton code dans l’éditeur de WordPress sans tester, car là, tu as pas pu le faire fonctionner comme ça, je corrige juste une ligne mais qui sans elle, ne supprime pas les éléments :

    
    $( "#conf_presents" ).find('li .erase').on('click',function(){
    

    devient

    
    $( "#conferenciers_presents" ).find('li .erase').on('click',function(){
    

    puisque #conf_present est un INPUT est non pas une DIV ;)

    Aussi, correction sécu+fonctionnelle : Ne jamais afficher des infos sans les sanitizer !
    Voici la modif :

    
    var availableTags = [ID.'",label:"'.$cf->post_title.'"},'."\n";
    	}
    ?>];
    

    devient :

    
    var availableTags = [ID.'",label:"'.esc_js($cf->post_title).'"},'."\n"; ////
    	}
    ?>];
    

    J’ai ajouté « esc_js() » qui est là pou « escaper » du contenu pour le « js ».
    Fait le test avec un conférencier qui contient une double quote (« ) dans son nom, et boum, tu te retrouves avec de la bad concat en JS, forcément ça pète ;)
    J’ai supprimé le order by title car on s’en fiche bien gras, et en plus, le tri sur une chaine est moins bon en perf que sur une date (me trompe-je Rahe ?)

    Et comme j’ai dis au dessus :

    
    if($conferenciers_presents[0]!=NULL){
    

    devient

    
    if( isset( $conferenciers_presents[0] ) ){
    

    Et pour info, j’ai du ne pas mettre le CSS perso car ça me fait un carré voir avec texte en noir 😮

    Bravo pour ce tuto tout de même, par contre, je pense qu’un débutant sera perdu car tu dis « créez une balise SCRIPT dans la meta box », mais c’est quoi la metabox ? je la mets ou ? etc, je te conseille de donner le code complet, final à utiliser. En ajoutant bien sur des recommandations comme « mon custom post type est ‘conférencier’ à vous de mettre le votre ».
    Dans tous les cas, pour un dev WP, ça psoe pas de soucis, j’ai réussi à le faire fonctionner ;p
    Allez je jete un oeil pour mon find_posts_div() ;)

  8. Par Julio Potier (BoiteAWeb) — Il y a 5 années
    Bon, ça foire de mettre du code

    >_ID.'",label:"'.$cf->post_title.'"},';

    devient

    echo '{value:"'.$cf->ID.'",label:"'.esc_js($cf->post_title).'"},'."\n";

    j’ai ajouté « \n » aussi pour que dans le source on y voit clair :p

  9. Par Willy Bahuaud — Il y a 5 années
    Oui effectivement, tu m’as démasqué, j’ai modifié mon code sans le tester ^^ désolé.

    Je suis conscient que ce ne doit pas être très clair pour les débutants, j’espère progresser dans la rédaction des prochains articles 🙂 (ainsi qu’en sécu^^)

    Et sinon je tiens à dire merci pour vos remarques !!

    Pour les requêtes en ajax avec $.findPosts() je vais tester, ne t’embêtes pas à faire une démo 🙂

  10. Par Willy Bahuaud — Il y a 5 années
    j’ai mis l’article à jour en tenant compte de tes remarques de code.
    Seul petit détail, pour le test :

    
    if( isset( $conferenciers_presents[0] ) ){
    //...
    

    j’ai préféré faire :

    
    if( !empty( $conferenciers_presents ) ){
    //...
    

    Je pense que ça revient au même.

    Sinon c’est le cinquième article sur les metabox que je fais. C’est juste pour moi l’occasion de partager des bouts de code que j’utilise pas mal au quotidien.

    Des fois des plugins peuvent faire la même chose, et je réinvente sans doute la roue… mais au moins je sais ce qu’ils ont dans le ventre, et ils font exactement ce dont j’ai besoin. Ils ne m’ajoutent pas des dizaines d’options (ce que j’apprécie fortement ;-)) et quelque part je trouve ça plus clair pour mes clients.

    J’ai prévu un dernier article dans la série mais j’annonce la couleur : ça fera exactement la même chose que “next gen gallery” mais en plus simple… J’vais pas le faire tout de suite car je pars en vacances, mais à mon retour 🙂

    Après ça sera l’occaz’ de parler d’autres choses que de metabox

    en attendant je vais aller jeter un oeil du côté de “media.dev.js”

  11. Par Julio Potier (BoiteAWeb) — Il y a 5 années
    Ok, pour empty(), ça marche dans ton cas, mais attention car elle retourne faux si la variable testé evaut 0 ou « 0 » ou «  » ou FALSE ou array(), alors que isset() renvoie vrai pour tout ça.
    (empty() renvoie vrai aussi pour NULL et pour une variable déclarée mais non ‘remplie’)
    => empty() ne vaut pas isset() ;)
  12. Par Bertrand — Il y a 4 années
    Salut Willy, Merci pour ce tuto qui m’a grandement aidé.

    Je ne sais pas si tu l’as remarqué mais lors de la sauvegarde si aucun conférencier n’est entré le nom de l’article est automatiquement enregistré comme conférencier. Je tente depuis plusieurs heure de régler cela mais sans succès

  13. Par julie — Il y a 4 années
    Coucou,

    merci pour cet article et merci surtout pour les commentaires… Je viens d’apprendre la différence entre !empty et isset alors que j’ai toujours été persuadé que la fonction faisait la même chose.

    Et félicitation pour ton blog, c’est rare de tomber sur un design si abouti.

  14. Par yves — Il y a 3 années
    merci.
  15. Par Helo — Il y a 3 années
    Merci pour tous ces articles sur les meta box, j’apprends pleins de choses =)

    Par contre le lien pour télécharger les style semble mort, est-il possible de ré-hébergé le fichier ?

    Merci
    et bonne continuation pour cet excellent site.

    Hélo

Commenter