Edit in Place avec jQuery

Nous allons dans ce tutoriel apprendre comment créer un système d'edit-in-place avec jQuery. Deux questions se pose à nous quand à ce titre. Qu'est-ce qu'un système d'edit-in-place et qu'est-ce que jQuery ? L'edit-in-place est une technique permise par AJAX permettant de modifier le contenu d'une page à la volé, c'est-à -dire qu'on pourra modifier la page sans la recharger et que les modifications seront faites en temps réel. Ceci est fait avec du JavaScript qui nous permettra d'afficher des champs d'édition afin de pouvoir modifier les informations. Au tour de jQuery, qu'est-ce donc ? Et bien c'est ce qu'on appelle un Framework, en gros, c'est un outils mettant à notre disposition plein de fonctions JavaScript très puissantes qui vont grandement nous simplifier le travail. D'ailleurs, si le slogan de jQuery est "Write less, do more", ce n'est pas pour rien !


L'application que nous allons utiliser est la simple interface d'administration d'un magasin de vente en ligne. Nous allons imaginer que nous avons une superbe bibliothèque en ligne et nous sommes chargé de mettre de l'ordre dans tout ça. Pour faire clair, nous avons un tableau qui affiche les livres mis en vente avec la couverture des livres, leur identifiant (dans la base de donnée), leur titre, l'auteur, le prix et une description.

Notre application

Nos livre sont stockées dans une base de données MySQL. Je vous donne tout de suite le code de la table qui contiendra les ouvrages.

--
-- Structure de la table `eip_books`
--

CREATE TABLE IF NOT EXISTS `eip_books` (
 `eip_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Unique identifier of the book',
 `eip_image` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'URL of books cover image',
 `eip_title` varchar(225) COLLATE utf8_unicode_ci NOT NULL COMMENT 'title of the book',
 `eip_author` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'Author of the book',
 `eip_price` float unsigned NOT NULL COMMENT 'Price of the book',
 `eip_description` text COLLATE utf8_unicode_ci NOT NULL COMMENT 'Description of the book',
 `eip_ip_lastmodifier` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT 'IP address of the last modifier',
 KEY `eip_id` (`eip_id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='List of books for the application demo';

Nous avons une simple page HTML avec un tableau affichant les livres.

<?php
// Inclusion des informations necessaires
require_once('config/config.conf.php');
require_once('php/Book.class.php');
?>
<!DOCTYPE html>
<html>  
<head>  
   <title>Système d'edit-in-place avec jQuery</title>  
   <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
   <link rel="shortcut icon" type="image/x-icon" href="./favicon.ico" />
   <link rel="stylesheet" type="text/css" href="css/design.css" media="screen" />  
   <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
   <script type="text/javascript" src="js/books.js"></script>
</head>  
<body>
  <div id="header">  
    <div id="header-logo">
      <a href="http://demo.syrinxoon.net">
        <img src="http://demo.syrinxoon.net/asset/img/logo.png" alt="Syrinxoon" title="Retour a l'accueil" width="414" height="197" />
      </a>
    </div>
    <div id="header-text">
      <h1>Système d'edit-in-place avec jQuery</h1>
    </div>
    <div class="clear"></div>
  </div>

  <div id="page">
    <h2>Liste des ouvrages disponibles sur le magasin en ligne</h2>
    <div class="ajax-response"></div>
    <?php
      $book = new Book();
      $book->showBooks();
    ?>
    <div class="ajax-response"></div>
  </div>
</body>  
</html>

Nos livres sont affichés via la méthode showBooks() de la class Book. Je ne vais pas la détailler ici car ce n'est pas l'objet de ce tutoriel. En revanche, je vais expliquer comment se compose le code HTML du tableau car nous en aurons besoin pour ce qu'on souhaite faire.

<tr id="book-1">
   <td><img src="images/livre-html-css.jpg" alt="#" width="100" /></td>
   <td>#1</td>
   <td id="book-title-1">Premiers pas en CSS et XHTML</td>
   <td id="book-price-1">15&euro;</td>
   <td id="book-author-1">Francis Draillard</td>
   <td id="book-description-1" class="description">Le design d'un site ne se suffit pas à  lui-même, encore faut-il penser à  la maintenance : rectifications, mise à  jour, changements de mise en page doivent pouvoir s'effectuer rapidement. C'est pourquoi, Francis Draillard, fort de son expérience en conception web et en qualité de fin pédagogue, vous fait découvrir l'efficacité des CSS et du XHTML tout en douceur. Feuilles de style, mise en forme, blocs, intégration de média : vous découvrirez tout pour bien démarrer !
   </td>
   <td>
      <p id="editbox-button-1">
        <img src="images/table_edit.png" alt="Editer" class="button edit-button" id="edit-book-1" />
      </p>
      <p id="editbox-commands-1" style="display: none;">
        <img src="images/tick.png" alt="Valider" id="edit-book-validate-1" class="button valider" /> ou
        <img src="images/cross.png" alt="Annuler" id="edit-book-cancel-1" class="button annuler" />
      </p>
   </td>
</tr>

Une ligne du tableau correspond à un livre. Chaque ligne à un identifiant commençant par book- suivi de l'id du livre dans la base de données (On évitera le jeu de mot sur book-1). Ensuite, chaque cellule de la ligne a un identifiant, sauf celle de l'image car nous n'aurons pas besoin de la manipuler avec JavaScript. La cellule contenant le titre a l'id book-title-IDLIVRE celle du prix l'id book-price-IDLIVRE, celle de l'auteur book-author-IDLIVRE, et celle contenant la description book-description-IDLIVRE. La dernière cellule est divisée en deux parties et contient les boutons d'édition. Le paragraphe ayant l'identifiant book-button-IDLIVRE contient le bouton permettant de lancer l'édition du livre, et le paragraphe book-commands-IDLIVRE contient les deux boutons permettant de confirmer ou d'annuler la modification. Grâce à ces identifiants, nous pourrons manipuler chaque cellule du tableau dont nous avons besoin.

Tout le code créé avec jQuery doit être contenu dans un fonction globale se déclenchant lorsque la page est chargée. Quel que soit ce que vous faites, vous devez toujours mettre votre code jQuery dans cette fonction. Créez donc un fichier appelé books.js et mettez-y le code suivant.

$(document).ready(function() {
  // Votre code JavaScrit ici
});

Vous vous souvenez du bouton contenu dans le paragraphe book-button-IDLIVRE dont je vous ai parlé tout à l'heure ? Et bien ce bouton contient lui aussi un id, edit-book-IDLIVRE ainsi qu'une classe edit-button. La fonction qui lancera l'edit-in-place est déclenchée lorsqu'on clique sur ce bouton, c'est tout simple avec jQuery et ça se fait comme ça :

// book.js
// Affichage des champs d'edition
$('#books-list .edit-button').click(function() {
  // On commence par recuperer l'identifiant du livre
  var idHtml = $(this).attr('id');
  var ids = idHtml.split('-');
  var id = ids[2];
...

La première chose que nous avons à faire est de récupérer l'identifiant du livre que nous souhaitons éditer. Pour ce faire, nous allons récupérer l'identifiant du bouton qui contient, je vous le rappelle, l'identifiant de l'article. à‡a se fait une fois encore de façon très simple, nous récupérons l'id grâce à la fonction attr() qui prend comme paramètre l'attribut que nous souhaitons récupérer, soit ici id. Nous appliquons cette fonction sur l'objet $('this') qui symbolise l'élément courant, soit ici le bouton sur lequel l'utilisateur vient de cliquer. Nous récupérons donc l'id sous la forme d'une chaine de caractères, mais seul l'identifiant numérique situé en fin de chaine nous intéresse. Nous allons donc exploser la chaine au niveau des tirets séparant chaque élément qui le compose grâce à la fonction split() qui prend comme paramètre le caractère séparant chaque élément que nous souhaitons récupérer. On se retrouve donc avec un tableau, l'identifiant numérique étant à sa fin. Maintenant que nous avons récupérer l'identifiant, on va pouvoir passer aux modifications du code HTML permettant l'edit-in-place. On commence par cacher le bouton d'édition et afficher les boutons de confirmation ou d'annulation.

// book.js
// On compte le nombre de boutons d'edition de la page et on les cache tous pour eviter le multi-edit
var editButtonNb = $('.edit-button').length;
for (i = 1; i != editButtonNb + 1; i++) {
  $('#editbox-button-' + i).hide();
}
// Et on affiche les commandes d'edition
$('#editbox-commands-' + id).show();
...

Je ne me contente pas de cacher le bouton d'édition de l'article qu'on modifie mais je les cache tous grâce à une boucle. Pourquoi ? Tout simplement pour éviter le multi-edit qui pourrai occasionner des problèmes. Ensuite, on récupère le contenu actuel de chaque cellule éditable grâce à la fonction html() qui si on ne lui fournit pas de paramètre, retournera le code html situé à l'intérieur de la cellule. Comme nous connaissons l'id des cellules, nous n'avons plus qu'à appliquer la fonction html() sur chacune d'elles :

// book.js
// Maintenant, on va sauvegarder les textes actuels au cas ou on annulerait la modification
var oldTitle = $('#book-title-' + id).html();
var oldPrice = $('#book-price-' + id).html();
var oldAuthor = $('#book-author-' + id).html();
var oldDescription= $('#book-description-' + id).html();

// Pour afficher le prix, on supprime le caractere euro qui fait parti de la chaine
var priceLength = oldPrice.length;
var displayPrice = oldPrice.substr(0, priceLength - 1);
...

Une fois que nous avons stocké l'ancien contenu, nous allons le remplacer par des champs de texte dans lesquels nous allons remettre l'ancien contenu, ce qui évite de tout devoir réécrire. Vous pouvez noter que je passe un peu plus de temps sur le prix car le contenu de la cellule contenait le caractère euro. Comme il ne nous intéresse pas, je le supprime.

// books.js
// Puis on affiche les champs de texte
$('#book-title-' + id).html('').html('<input type="text" id="new-title-' + id + '" size="20" value="' + oldTitle + '" />');
$('#book-price-' + id).html('').html('<input type="text" id="new-price-' + id + '" size="4" value="' + displayPrice + '" />');
$('#book-author-' + id).html('').html('<input type="text" id="new-author-' + id + '" size="20" value="' + oldAuthor + '" />');
$('#book-description-' + id).html('').html('<textarea id="new-description-' + id + '" cols="40" rows="12">' + oldDescription + '</textarea>');
...

Nous utilisons une fois encore la fonction html(), mais en lui passant comme paramètre ce que nous souhaitons insérer dans la cellule. A présent nous pouvons apporter nos modifications aux informations du livre. Je vous donne le code complet de cette première fonction.

// book.js
// Affichage des champs d'edition
$('#books-list .edit-button').click(function() {
  // On commence par recuperer l'identifiant du livre
  var idHtml = $(this).attr('id');
  var ids = idHtml.split('-');
  var id = ids[2];

  // On compte le nombre de boutons d'edition de la page et on les caches tous pour eviter le multi-edit
  var editButtonNb = $('.edit-button').length;
  for (i = 1; i != editButtonNb + 1; i++) {
      $('#editbox-button-' + i).hide();
  }
  // Et on affiche les commandes d'edition
  $('#editbox-commands-' + id).show();

  // Maintenant, on va sauvegarder les textes actuels au cas ou on annulerait la modification
  var oldTitle = $('#book-title-' + id).html();
  var oldPrice = $('#book-price-' + id).html();
  var oldAuthor = $('#book-author-' + id).html();
  var oldDescription= $('#book-description-' + id).html();

  // Pour afficher le prix, on supprime le caractere euro qui fait parti de la chaine
  var priceLength = oldPrice.length;
  var displayPrice = oldPrice.substr(0, priceLength - 1);

  // Puis on affiche les champs de texte
  $('#book-title-' + id).html('').html('<input type="text" id="new-title-' + id + '" size="20" value="' + oldTitle + '" />');
  $('#book-price-' + id).html('').html('<input type="text" id="new-price-' + id + '" size="4" value="' + displayPrice + '" />');
  $('#book-author-' + id).html('').html('<input type="text" id="new-author-' + id + '" size="20" value="' + oldAuthor + '" />');
  $('#book-description-' + id).html('').html('<textarea id="new-description-' + id + '" cols="40" rows="12">' + oldDescription + '</textarea>');

  /* Les fonctions suivantes prendront place ici */

});

Affichage de l'edit-in-place

Une fois nos modifications terminés, on aimerait bien qu'elles soient sauvegardées. Pour ce faire, on va cliquer sur le bouton de validation. Tout comme le bouton d'édition, le bouton de validation contient un id edit-book-validate-IDLIVRE ainsi qu'une classe validate. Comme pour la première fonction, la seconde se déclenchera lorsque l'utilisateur cliquera sur le bouton. On commence par afficher un message de chargement puis nous récupérons de nouveau l'identifiant numérique du livre. Cette fonction doit être déclarée dans la première fonction, juste après la ligne 30 du code source ci-dessus.

// books.js
$('#books-list .valider').click(function() {
  var idHtml = $(this).attr('id');
  var ids = idHtml.split('-');
  var id = ids[3];
  // On commence par envoyer un message de chargement en haut et en bas du tableau (dans le cas d'un tableau long)
  $('.ajax-response').html('<img src="images/loader.gif" alt="Loading" /> Enregistrement des modifications en cours...');

  // On recupere ensuite le contenu des champs
  var title = $('#new-title-' + id).val();
  if (title.length == 0) {
    $('.ajax-response').html('').html('<div class="error">Le titre ne peut être vide !</div>');
    return false;
  }
  var price = $('#new-price-' + id).val();
  if (price.length == 0 || price == 0) {
    $('.ajax-response').html('').html('<div class="error">Le prix ne peut être nul !</div>');
    return false;
  }
  var author = $('#new-author-' + id).val();
  if (author.length == 0) {
    $('.ajax-response').html('').html('<div class="error">L'auteur doit être donné !</div>');
    return false;
  }
  var description = $('#new-description-' + id).val();
  if (description.length == 0) {
    $('.ajax-response').html('').html('<div class="error">La description ne peut être vide !</div>');
    return false;
  }
...

Le début de la fonction est assez similaire à la première, nous récupérons donc les modifications apportées par l'utilisateur grâce à la fonction val() qui a exactement le même fonctionnement que html(), mais qui s'applique aux champs de formulaire. La différence avec tout à l'heure, c'est que nous allons contrà´ler que l'utilisateur ait bien rempli tous les champs. La cas contraire, nous affichons un message d'erreur et nous annulons la procédure en retournant false. Comme ces vérifications peuvent être facilement contournées, nous les referons avec PHP cà´té serveur. C'est ensuite que les chose se compliquent (un tout petit peu). Nous allons initialiser un objet ajax comprenant 5 paramètres :

  • url : chemin vers le fichier PHP qui sera appelé de manière asynchrone
  • type : méthode d'envoi des données (GET ou POST)
  • data : objet ayant des paires clés / valeurs, il contient les données que nous envoyons à PHP
  • success : fonction de callback déclenchée lorsque la requête aboutie
  • error : fonction de callback déclenchée lorsque la requête plante

Ensuite, nous n'avons plus qu'à passer à l'objet data les variables contenant les données que nous venons de récupérer. La suite se passe côté serveur, je le détaillerai plus loin dans ce tutoriel.

// books.js
  /*
  On definit ensuite les propriete de l'objet ajax avec l'adresse du fichier qui va traiter la requete,
  le format d'envois (GET ou POST), puis on transmet nos variables via un objet
  */
  $.ajax({
    url: 'books.php',
    type: 'POST',
    data: {
      book_id : id,
      book_title : title,
      book_price : price,
      book_author : author,
      book_description : description
    },
    success: function(retour) {
      // On va tester le retour pour savoir si on a une erreur
      var verif = new RegExp("^<div class="error">$", "i");
      if (verif.test(retour)) {
        $('.ajax-response').html('').html(retour);
      }
      else {
        // Si ce n'est pas le cas, on affiche les nouvelles informations
        $('.ajax-response').html('').html(retour);
        $('#book-title-' + id).html('').html(title);
        $('#book-price-' + id).html('').html(price + '&euro;');
        $('#book-author-' + id).html('').html(author);
        $('#book-description-' + id).html('').html(description);    
        $('#editbox-commands-' + id).hide();

        // Et on affiche les boutons d'edition
        for (i = 1; i != editButtonNb + 1; i++) {
            $('#editbox-button-' + i).show();
        }        
      }
    },
    error: function(obj, str, except) {
      $('.ajax-response').html('').html('<div class="error">Echec de la requête...</div>');    
      alert(str);
    }
  });
// Fin de la seconde fonction
});

La fonction success retourne une valeur contenant le résultat envoyé par le script PHP. Lorsque cette fonction est déclenchée, nous insérons le contenu modifié dans les cellules , nous cachons les boutons de confirmation et réaffichons le bouton d'édition puis on affiche le message envoyé par le script PHP. Si la requête plante, la fonction error sera déclenchée, nous affichons alors un message d'erreur qui ne sera hélas pas très explicite mais l'erreur concerne la communication avec le script PHP.

Si l'utilisateur réalise qu'il ne voulait pas modifier le livre, il clique sur le bouton annuler qui déclenchera une troisième fonction identique à la seconde fonction, à l'exception faite qu'elle n'enverra rien au script PHP. Cette fonction réinsère dans les cellules les anciennes valeurs que nous conservons depuis le début de l'édition. Et voilà, c'est tout ce que nous à faire du cà´té du JavaScript. Grâce à jQuery, nous avons économisé plusieurs dizaines de lignes de code, ce qui n'est pas négligeable.

// books.js
// Dans le cas ou il annule les modifications...
$('#books-list .annuler').click(function() {
  var idHtml = $(this).attr('id');
  var ids = idHtml.split('-');
  var id = ids[3];

  // On supprimer les champs et on affiche les anciennes valeurs
  $('#book-title-' + id).html('').html(oldTitle);
  $('#book-price-' + id).html('').html(oldPrice);
  $('#book-author-' + id).html('').html(oldAuthor);
  $('#book-description-' + id).html('').html(oldDescription);
  // Enfin, on cache les boutons de confirmation ou d'annulation et ou affiche le bouton d'edition
  $('#editbox-commands-' + id).hide();
  for (i = 1; i != editButtonNb + 1; i++) {
    $('#editbox-button-' + i).show();
  }
});

Le script PHP recevant les données envoyées via JavaScript se nomme books.php.

# books.php
<?php
require_once('config/config.conf.php');
require_once('php/Book.class.php');

if (isset($_POST) && !empty($_POST)) {
  // Verifications
  if (strlen($_POST['book_title']) == 0) {
    echo '<div class="error">Le titre ne peut être vide !</div>';
    exit();
  }
  elseif (strlen($_POST['book_price']) == 0 || $_POST['book_price'] == 0) {
    echo '<div class="error">Le prix ne peut être nul !</div>';
    exit();
  }
  elseif (strlen($_POST['book_author']) == 0) {
    echo '<div class="error">Vous devez entrer un auteur !</div>';
    exit();
  }
  elseif (strlen($_POST['book_description']) == 0) {
    echo '<div class="error">Vous devez entrer une description !</div>';
    exit();
  }
  else {
    // Si tout est bon, on continue
    $book = new Book();
    if ($book->editBook($_POST)) {
      echo '<div class="accept">Le livre ' . $_POST['book_titre'] . ' a été modifié avec succès !';
      exit();
    }
    else {
      echo '<div class="error">Une erreur est survenue lors de la requête</div>';
      exit();
    }
  }
}
else {
  echo '<div class="error">Vous n\'avez envoyé aucune donnée. Repassez quand vous en aurez !</div>';
  exit();
}
?>

On commence par inclure le fichier de configuration contenant entre autre les informations de connexion à la base de données. Nous incluons également la class Book dont la méthode editBook() nous permettra de sauvegarder simplement nos modifications. Ensuite, nous vérifions les données comme on l'a fait en JavaScript avant. Si aucune erreur n'a été détectée, on appelle la méthode editBook() qui prendra comme paramètre le tableau $_POST contenant les données. Voyons maintenant en détails le contenu de cette méthode.

#Book.class.php
<?php
// Importation du fichier de configuration
require_once('config/config.conf.php');
// Importation de la classe de connexion MySQLi
require_once($config['paths']['phpDir'] . 'Ressource.class.php');
// Importation des requetes preparees
require_once($config['paths']['queriesFile']);

// On cree un class afin de grouper toutes les methodes dans un seul fichier
class Book {

  private $connexion;
  private $prepa;

  function __construct() {
      global $config;
      global $queries;
      $this->config = $config;
      $this->queries = $queries;


      if ($connexion = new Ressource($this->config['db1']['host'], $this->config['db1']['user'], $this->config['db1']['passwd'], $this->config['db1']['base'])) {
          $this->connexion = $connexion;
          $prepa = $this->connexion->stmt_init();
          $this->prepa = $prepa;

          return true;
      }
      else {
          $this->alerteMsg = $connexion->connect_error;
          return false;
      }
  }

  function __destruct() {
      $this->prepa->close();
      $this->connexion->close();
  }

  public function editBook($data) {

    $data = $this->connexion->protectData($data, $this->connexion);

    $query = $this->queries['editBook'];
    $this->prepa->prepare($query);
    $this->prepa->bind_param('sisssi', $data['book_title'], $data['book_price'], $data['book_author'], $data['book_description'], $_SERVER['REMOTE_ADDR'], $data['book_id']);

    if ($this->prepa->execute()) {
      return true;
    }
    else {
      return false;
    }
  }
}

?>

Vous remarquerez tout de suite que j'utilise les requêtes préparées avec STMT de MySQLi. On commence par sécuriser les données envoyées par l'utilisateur grâce à la méthode protectData() appartenant à la classe Ressource qui est en fait une extension de la class mysqli. Cette méthode prend deux paramètres, le premier étant le tableau de données, le second la ressource (la connexion à la base de données). Voici le code de cette méthode :

#Ressource.class.php
<?php
class Ressource extends mysqli {

  public function protectData($data, $connection) {

    $protectedData = array();

    for(reset($data); current($data); next($data)) {

      $element = mysqli_real_escape_string($connection, current($data));
      $element = addcslashes(current($data), '%_');
      $element =  htmlspecialchars(current($data), ENT_QUOTES);
      $protectedData[key($data)] = $element;

    }

    return $protectedData;
  }
}
?>

Ensuite, nous sélectionnons la requête que nous allons avoir besoin pour éditer les données. Elle est dans un fichier queries.conf.php contenant un tableau listant toutes les requêtes dont nous avons besoin. Voici la requête :

UPDATE eip_books SET eip_title=?, eip_price=?, eip_author=?, eip_description=?, eip_ip_lastmodifier=? WHERE eip_id=?

Comme nous utilisons les requêtes préparées, toutes les valeurs sont représentées par un point d'interrogation, lequel sera remplacé par PHP dans quelques lignes. Vient ensuite la méthode prepare() qui prend comme paramètre la requête SQL et prépare cette dernière. Puis nous remplaçons les ? de notre requête par les valeurs grâce à la méthode bind_param() prenant en compte dans un premier paramètre une chaine de caractère représentant le type des données : s pour string (chaine de caractères) et i pour integer (chiffre). Vous listez ensuite les variables à lier à la requête. Attention : les variables doivent être passées dans l'ordre de la requête SQL, idem pour les types. Enfin nous exécutons notre requête à l'aide de la méthode execute(). Si la requête est un succès, nous retournons true, false dans le cas contraire. Puis nous retournons à notre script books.php qui vérifie que editBook() a retourné true. Si ce n'est pas le cas, nous affichons une erreur. Dans le cas où tout s'est bien déroulé, on affiche un message de confirmation.

Message de confirmation

Et voilà un exemple concret d'application de l'edit in place. Vous voyez qu'il n'est pas bien compliqué de créer un système de ce genre. De plus, jQuery nous a grandement simplifié la tâche.


Abonnez vous au flux RSS des tutoriels pour rester informé.
Sinon, n'hésitez pas à laisser un commentaire ou à partage ce tutoriel sur les réseaux sociaux, ça me fait toujours plaisir et m'encourage à continuer mon oeuvre pour un monde meilleur.

Ajouter un commentaire