Modification "inline" de données avec AJAX
Date de publication : 15/08/06
Par
Olivier Lance (Accueil)
Ce tutoriel a pour but de vous montrer comment mettre en oeuvre simplement un script de modification "inline" de
vos données affichées dans une page web.
Introduction
I. Première partie : Création du script "simple"
I-1. Analyse préliminaire
I-2. Côté client
I-2-a. La page web
I-2-b. Le Javascript
I-2-b-i. Mode d'édition
I-2-b-ii. Sauvegarde
I-3. Côté serveur
II. Deuxième partie : Généralisation du script, emploi de classes
II-1. Les modifications apportées
II-1-a. Limites du script actuel
II-1-b. Pourquoi des classes ?
II-2. Adaptation du code d'origine
II-2-a. Réorganisation des fichiers
II-2-b. Modification de index.php
II-2-c. Fonctionnalités des classes
II-2-d. Modification d'inlinemod.js
II-2-d-i. La fonction inlineMod
II-2-d-ii. La fonction sauverMod
II-2-e. Modification de sauverMod.php
II-3. Implémentation des classes
II-3-a. L'objet texte
II-3-b. L'objet nombre
II-3-c. L'objet texteMulti
Conclusion
Liens
Addenda
Constantes utilisées pour les accès BDD
Introduction
De plus en plus, la "mode" pousse les développeurs à créer des pages web dynamiques et interactives construites pour un
confort d'utilisation toujours augmenté pour l'utilisateur final.
Aujourd'hui ce confort passe, entr'autres, par l'emploi du
Javascript et de l'objet
XMLHTTPRequest, qui permet d'effectuer
des requêtes vers le serveur web de manière asynchrone. Couplé avec quelques scripts PHP, il permet de mettre à jour
des informations au sein d'une page sans en recharger l'intégralité du contenu.
C'est cette méthode que je vous propose d'utiliser pour mettre en place un système de
modification inline de
données dans une page web.
Par
modification inline, j'entends modification d'éléments distincts de la page, directement à leur emplacement
d'origine.
Pour bien vous rendre compte de l'idée, vous pouvez d'ores et déjà trouver
ici
un exemple fonctionnel du résultat de ce tutoriel. L'utilisation est simple : en double-cliquant sur une donnée du tableau,
vous entrez en mode d'édition. Vous pouvez alors changer la valeur de la donnée, puis valider votre saisie en appuyant
sur Entrée ou en cliquant à l'extérieur du champ de texte.
Actualisez, la valeur que vous avez entrée est sauvegardée !
Ce tutoriel est découpé en deux parties : la première partie vous guidera et vous expliquera les étapes de création de
ce script d'édition. La seconde partie, pour les plus chevronné(e)s, reprendra le code de la première partie pour le
généraliser et permettre une plus grande flexibilité d'implémentation grâce à l'utilisation de classes.
Dans tout le tutoriel, je considèrerai que vous êtes à l'aise avec l'utilisation de l'objet XMLHTTPRequest et avec tout
ce qui concerne les appels de requêtes MySQL à partir de PHP, de leur création à l'exploitation de leur résultat.
 |
Depuis un moment, mon script comprenait une erreur qui empêchait l'enregistrement dans la table des modifications apportées
par l'utilisateur.
Je tiens à m'excuser pour ce problème et le temps que j'ai pris à le résoudre. Je vous propose de télécharger ce zip qui
contient une version des fichiers qui fonctionne correctement en local sur ma machine.
(en cas de problème avec le lien précédent : miroir)
|
I. Première partie : Création du script "simple"
I-1. Analyse préliminaire
Comme vous avez pu le voir dans la page donnée à l'introduction, l'idée est simple : lors d'un double-click sur l'une
des cellules du tableau, son contenu est remplacé par un champ de saisie qui prend pour valeur le texte de la cellule.
Le double-click permet donc de passer d'un mode d'affichage à un mode d'édition.
Dans ce mode d'édition, la valeur affectée est librement modifiable, puis enregistrée soit en appuyant sur la touche
Entrée, soit en sortant du champ de saisie.
De ces simples constats on peut donc tirer l'analyse descendante suivante :

Analyse générale
L'affichage des données se fait dans un premier temps grâce à la page web dont le code sera présenté peu après. Lors du
retour vers le mode d'affichage après la sauvegarde des données, nous devrons user d'un peu de Javascript afin de
supprimer le champ de saisie et de remettre le texte qu'il contenait dans la cellule.
Le passage en mode d'édition peut être découpé en plusieurs actions principales :

Analyse du mode d'édition
Dans un premier temps, avant de remplacer le contenu de la cellule, il faut en garder une copie pour pouvoir l'assigner
au contenu du champ de saisie.
Ensuite, le champ de saisie est créé. Avant de l'afficher, il conviendra de modifier quelques unes de ses propriétés :
sa taille, son style CSS...
Et enfin, une fois le champ affiché, on sélectionne son contenu et on lui donne le focus pour que l'édition
puisse être immédiatement effectuée au clavier.
Selon le type de valeur à modifier (texte, texte "long" pouvant s'étendre sur plusieurs lignes, nombre...), nous
introduirons un comportement différent. Pour ce tutoriel, les textes et nombres seront modifiés grâce à un champ de
saisie classique (balise input) et les textes longs seront modifiés grâce à une zone de texte multilignes
(balise textarea). Il faudra bien sûr, à un moment ou un autre, spécifier quel est le type de la donnée affichée.
De même, la phase de sauvegarde peut être ainsi découpée :

Analyse de la sauvegarde
Cette phase est relativement simple. Une fois l'objet XMLHTTPRequest créé, nous appelons un script PHP en lui passant
en paramètres les valeurs à sauver, puis nous sortons du mode d'édition une fois la sauvegarde effectuée.
Afin de garder un script relativement général, nous ne différencierons pas la requête selon le type du champ modifié,
ou bien selon le nom du champ correspondant dans la base de données (ce qui reviendrait à faire une requête par champ).
Nous passerons donc en paramètres au script le nom du champ dans la base de données, son type, sa valeur et bien sûr
l'id de l'enregistrement dont il faut modifier le champ.
Comme vous pouvez le voir dans le diagramme précédent, nous ne ferons pas de gestion d'erreur de la sauvegarde dans
cette première partie. Je considère que vous savez utiliser l'objet XMLHTTPRequest et ses propriétés pour arriver à
vos fins sur ce plan.
La sortie du mode d'édition se fait en deux étapes principales : remplacement du champ de saisie par son contenu dans
la cellule en cours d'édition, puis suppression du champ.
Nous avons dorénavant en main tous les élements pour passer à la réalisation des différents codes qui permettront de
donner corps à ce système d'édition inline.
I-2. Côté client
La page web et le script PHP (côté serveur) qui seront utilisés sont bien sûr spécifiques à l'exemple que j'entends
traiter dans ce tutoriel. Avant toute chose, il me semble donc judicieux de donner dès maintenant la structure de la
table qui contiendra les données utilisées. Il s'agit d'une simple table recensant les états civils d'utilisateurs
fictifs :
CREATE TABLE `inlinemod` (
`id` int(11) NOT NULL auto_increment,
`nom` varchar(255) NOT NULL default '',
`prenom` varchar(255) NOT NULL default '',
`adresse` tinytext NOT NULL,
`code_postal` varchar(5) NOT NULL default '',
`ville` varchar(255) NOT NULL default '',
`enfants` int(11) NOT NULL default '0',
`email` varchar(255) NOT NULL default '',
PRIMARY KEY (`id`)
)
|
Dans cette table, tous les champs affichés et modifiables sont donc de type texte, à l'exception du champ
adresse qui sera de type texte-multi et du champ enfants qui sera de type nombre.
I-2-a. La page web
Le code de la page web de présentation des données est des plus basiques : une connexion à la base pour récupérer les
données à afficher, puis une boucle afin de construire le tableau contenant celles-ci. Voici déjà le code complet pour
une vue d'ensemble. Nous nous concentrerons ensuite sur les détails importants.
<?php
mysql_connect(DB_HOST, DB_USER, DB_PASSWORD) or die(mysql_error());
mysql_select_db(DB_NAME) or die(mysql_error());
$sql = 'SELECT * FROM `'.DB_TABLE_NAME.'`';
$result = mysql_query($sql) or die(__LINE__.mysql_error().$sql);
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=iso-8859-1" />
<title>Modification "inline" de données grâce à XMLHTTPRequest</title>
<link rel="StyleSheet" type="text/css" href="index.css"/>
<script type="text/javascript" src="inlinemod.js"></script>
</head>
<body>
<h1>Utilisateurs</h1>
<table id="table-utilisateurs">
<tr>
<th>Nom</th>
<th>Prénom</th>
<th>Adresse</th>
<th>Code Postal</th>
<th>Ville</th>
<th>Enfants</th>
<th>Email</th>
</tr>
<?php
while($user = mysql_fetch_assoc($result))
{
?>
<tr>
<td class="cellule" ondblclick="inlineMod(<?php echo $user['id']; ?>, this, 'nom', 'texte')">
<?php echo $user['nom']; ?>
</td>
<td class="cellule" ondblclick="inlineMod(<?php echo $user['id']; ?>, this, 'prenom', 'texte')">
<?php echo $user['prenom']; ?>
</td>
<td class="cellule" ondblclick="inlineMod(<?php echo $user['id']; ?>, this, 'adresse', 'texte-multi')">
<?php echo $user['adresse']; ?>
</td>
<td class="cellule" ondblclick="inlineMod(<?php echo $user['id']; ?>, this, 'code_postal', 'texte')">
<?php echo $user['code_postal']; ?>
</td>
<td class="cellule" ondblclick="inlineMod(<?php echo $user['id']; ?>, this, 'ville', 'texte')">
<?php echo $user['ville']; ?>
</td>
<td class="cellule" ondblclick="inlineMod(<?php echo $user['id']; ?>, this, 'enfants', 'nombre')">
<?php echo $user['enfants']; ?>
</td>
<td class="cellule" ondblclick="inlineMod(<?php echo $user['id']; ?>, this, 'email', 'texte')">
<?php echo $user['email']; ?>
</td>
</tr>
<?php
}
?>
</table>
</body>
</html>
<?php
mysql_close();
?>
|
Pour votre propre code vous aurez bien évidemment à définir les variables de connexion à la base de données, mais là
n'est pas notre sujet.
En premier lieu on remarque l'inclusion du fichier inlinemod.js, qui contiendra les fonctions utilisées pour le
mode d'édition et la sauvegarde.
Le plus important se situe au niveau des cellules du tableau, dont nous allons détailler les attributs. Pour référence
voici déjà une copie de la la balise d'ouverture de la première cellule :
<td class="cellule" ondblclick="inlineMod(<?php echo $user['id']; ?>, this, 'nom', 'texte')">
|
L'attribut class ne fait qu'assigner à la cellule une classe CSS que voici :
td.cellule {
text-align: center;
border: 1px solid #376ef9;
cursor: pointer;
}
|
La modification de l'apparence du curseur sur les cellules permet de signifier de manière simple à l'utilisateur qu'une
action est ici possible.
L'attribut ondblclick nous plonge plus en avant dans le sujet puisqu'il est le point d'entrée vers le mode
d'édition, grâce à la fonction inlineMod. Celle-ci prend quatre paramètres :
- L'id de l'enregistrement dans la base de données, afin de savoir quoi modifier lors de l'appel du script PHP
- Une référence sur l'objet qui contient la valeur à modifier. Ici, this est passé pour désigner la balise td
- Le nom du champ à modifier dans la base de données, toujours pour renseigner le script PHP
- Le type de la valeur. Ici elle est de type texte, plus bas dans la page vous trouverez également un type texte-multi et un type nombre
Voyons maintenant ce que donne, justement, l'implémentation de cette fonction inlineMod.
I-2-b. Le Javascript
I-2-b-i. Mode d'édition
Le code que je vais vous présenter ici ne respecte pas exactement l'ordre de l'analyse présentée ci-dessus.
L'esprit est le même bien sûr, mais le mode de fonctionnement du Javascript et notamment les possibilités offertes par la
manipulation du DOM permettent de regrouper certaines étapes.
Avant toute chose, afin qu'une seule édition ne soit effectuée à la fois, nous allons introduire une variable globale
de type booléen qui nous permettra de valider ou non le passage au mode d'édition suivant qu'une édition est déjà en
cours. Un test sur cette variable en début de fonction permettra ce contrôle :
var editionEnCours = false;
function inlineMod(id, obj, nomValeur, type)
{
if(editionEnCours)
{
return false;
}
else
{
editionEnCours = true;
}
|
Si la fonction n'est pas stoppée, nous pouvons alors créer notre champ de saisie en fonction du type de la
valeur à modifier :
var input = null;
switch(type)
{
case "texte":
case "nombre":
input = document.createElement("input");
break;
case "texte-multi":
input = document.createElement("textarea");
break;
}
|
Comme annoncé précédemment nous créons, grâce au DOM, une balise input pour les types texte et nombre,
et une balise textarea pour le type texte-multi.
L'élément créé peut ensuite être manipulé comme nous le désirons. Nous allons affecter le texte à éditer à sa
propriété value puis adapter sa taille à la largeur de ce texte :
if (obj.innerText)
input.value = obj.innerText;
else
input.value = obj.textContent;
input.value = trim(input.value);
input.style.width = getTextWidth(input.value) + 30 + "px";
|
Dans value est placé le contenu sous forme de texte de l'objet parent. Par souci de compatibilité, un test est
effectué pour savoir qui de innerText (Internet Explorer, Opera, Safari, Konqueror) ou textContent
(Firefox, ...) doit être utilisé. Comme Firefox renvoie également les sauts de lignes et espaces présents dans la balise
dont on appelle textContent, une fonction trim est utilisée pour supprimer ceux-ci. Son implémentation est
donnée dans le prochain extrait de code.
Afin d'adapter la taille du champ de saisie à son contenu, la fonction getTextWidth est utilisée. Il s'agit d'une
petite astuce utilisant le DOM et la propriété offsetWidth pour "mesurer" la taille d'un texte placé dans
une balise span :
function trim(value) {
var temp = value;
var obj = /^(\s*)([\W\w]*)(\b\s*$)/;
if (obj.test(temp)) { temp = temp.replace(obj, '$2'); }
var obj = / /g;
while (temp.match(obj)) { temp = temp.replace(obj, " "); }
return temp;
}
function getTextWidth(texte)
{
var largeur = 150;
if(trim(texte) == "")
{
return largeur;
}
var span = document.createElement("span");
span.style.visibility = "hidden";
span.style.position = "absolute";
span.appendChild(document.createTextNode(texte));
document.getElementsByTagName("body")[0].appendChild(span);
largeur = span.offsetWidth;
document.getElementsByTagName("body")[0].removeChild(span);
span = null;
return largeur;
}
|
Les commentaires du code devraient suffire à décrire cette fonction qui n'est pas d'une difficulté particulière. Une
fois les propriétés de notre champ ajustées, nous pouvons l'afficher dans la cellule, sélectionner son contenu et lui
donner le focus :
obj.replaceChild(input, obj.firstChild);
input.focus();
input.select();
|
Il reste maintenant à définir les événements qui déclencheront la sauvegarde de la saisie. La sortie du champ sera
détectée grâce à l'événement onblur, tandis que l'appui sur la touche Entrée sera vérifié grâce à l'événement
onkeydown et un test sur la touche appuyée lorsque l'événement est déclenché.
input.onblur = function sortir()
{
sauverMod(id, obj, nomValeur, input.value, type);
delete input;
};
input.onkeydown = function keyDown(event)
{
if (!event&&window.event)
{
event = window.event;
}
if(getKeyCode(event) == 13)
{
sauverMod(id, obj, nomValeur, input.value, type);
delete input;
}
};
|
Pour la compatibilité, quelques tests s'imposent sur la propriété event. L'important est la comparaison avec
le code caractère 13 qui représente le saut de ligne, et qui nous prévient donc d'un appui sur la touche Entrée.
La fonction getKeyCode renvoie ce code de caractère à partir de la propriété event :
function getKeyCode(evenement)
{
for (prop in evenement)
{
if(prop == 'which')
{
return evenement.which;
}
}
return evenement.keyCode;
}
|
La fonction inlineMod est maintenant terminée. Son fonctionnement correspond à l'analyse que nous en avions fait
en première partie, et il reste maintenant à implémenter la fonction de sauvegarde, sauverMod.
Cependant, vous l'avez peut-être déjà deviné, il existe un petit problème dans notre code. Après la sauvegarde, le champ
de saisie est supprimé. Si le champ possédait encore le focus à cet instant, cela causera donc le déclenchement de l'événement
onblur. Ainsi, si la sauvegarde est provoquée par l'appui sur la touche Entrée, la fonction sauverMod sera
appelée deux fois.
Pour pallier ce problème, nous allons introduire une seconde variable d'état qui marchera tout comme editionEnCours
pour vérifier que nous sommes déjà passés ou non par la fonction de sauvegarde.
Le code final de notre fonction inlineMod est donc :
| Fonction inlineMod |
var editionEnCours = false;
var sauve = false;
function inlineMod(id, obj, nomValeur, type)
{
if(editionEnCours)
{
return false;
}
else
{
editionEnCours = true;
sauve = false;
}
var input = null;
switch(type)
{
case "texte":
case "nombre":
input = document.createElement("input");
break;
case "texte-multi":
input = document.createElement("textarea");
break;
}
if (obj.innerText)
input.value = obj.innerText;
else
input.value = obj.textContent;
input.value = trim(input.value);
input.style.width = getTextWidth(input.value) + 30 + "px";
obj.replaceChild(input, obj.firstChild);
input.focus();
input.select();
input.onblur = function sortir()
{
sauverMod(id, obj, nomValeur, input.value, type);
delete input;
};
input.onkeydown = function keyDown(event)
{
if (!event&&window.event)
{
event = window.event;
}
if(getKeyCode(event) == 13)
{
sauverMod(id, obj, nomValeur, input.value, type);
delete input;
}
};
}
|
Tout est maintenant prêt pour l'implémentation de la fonction sauverMod.
I-2-b-ii. Sauvegarde
Notre fonction de sauvegarde s'articule autour de l'objet XMLHTTPRequest. Il convient donc de commencer par sa
création. Pour cela nous allons utiliser une fonction, getXMLHTTP, qui nous renverra une instance de l'objet selon
le navigateur utilisé.
function getXMLHTTP()
{
var xhr = null;
if(window.XMLHttpRequest)
{
xhr = new XMLHttpRequest();
}
else if(window.ActiveXObject)
{
try
{
xhr = new ActiveXObject("Msxml2.XMLHTTP");
}
catch(e)
{
try
{
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
catch(e1)
{
xhr = null;
}
}
}
else
{
alert("Votre navigateur ne supporte pas les objets XMLHTTPRequest...");
}
return xhr;
}
|
Je ne commenterai pas plus cette fonction. Elle est directement tirée du tutoriel de
Denis Cabasson,
un
autocomplétion pas à pas, que je ne peux que vous conseiller de lire ! (Voyez également les autres liens en bas
de page)
Voici comment nous allons créer notre objet :
var XHR = null;
function sauverMod(id, obj, nomValeur, valeur, type)
{
if(sauve)
{
return false;
}
else
{
sauve = true;
}
if(XHR && XHR.readyState != 0)
{
XHR.abort();
delete XHR;
}
XHR = getXMLHTTP();
if(!XHR)
{
return false;
}
|
Tout le code ne concerne pas directement la création de notre objet. Le premier bloc if, vous l'aurez compris,
permet d'invalider un appel à la fonction de sauvegarde si celle-ci a déjà été appelée pour la même phase d'édition.
XHR est déclaré comme une variable globale à laquelle nous assignons le résultat de getXMLHTTP. Avant cette
assignation, nous vérifions par sécurité que l'objet n'est pas déjà créé et en cours de transaction avec le serveur. Le
cas échéant, la connexion est tout simplement coupée et l'objet détruit avant d'être recréé.
En suivant l'analyse effectuée en première partie, il reste donc à envoyer la requête vers le script PHP et à sortir du
mode d'édition. Cela se fait de la manière suivante :
XHR.open("GET", "sauverMod.php?id=" + id + "&champ=" + nomValeur + "&valeur=" + escape(valeur) + "&type=" + type + ieTrick(), true);
XHR.onreadystatechange = function()
{
if (XHR.readyState == 4)
{
editionEnCours = false;
obj.replaceChild(document.createTextNode(valeur), obj.firstChild);
}
}
XHR.send(null);
|
Le script est appelé avec les paramètres déjà annoncés : l'id de l'enregistrement à modifier, le nom du champ
édité dans cet enregistrement, la valeur que celui-ci doit prendre et le type de cette valeur.
Le retour en mode affichage se fait dans l'événement onreadystatechange de XHR. Si l'appel du script PHP s'est
bien passé, et donc si readyState vaut 4, la variable editionEnCours est
réinitialisée (sauve l'étant au début de la fonction), et le contenu de l'objet parent du texte édité est
remplacé grâce au DOM.