Autocomplete en Ajax
CakePHP permet de mettre en place des requêtes en Ajax très facilement grâce au Helper AjaxHelper et l’utilisation du framework javascript Prototype et de son extension Script.aculo.us. Nous allons voir comment enrichir un champ texte pour qu’une aide à la complétion s’affiche lors de la saisie des premiers caractères.
1. Définition du projet
Imaginons un gestionnaire de personnes : lors de l’ajout ou de l’édition d’une personne, nous voulons renseigner sa profession. Nous pourrions proposer une liste déroulante avec une liste exhaustive des professions existantes, mais cette liste serait extrêmement longue et peu praticable. Nous allons remplacer cette liste par un simple champ texte, mais qui aura la particularité de proposer une liste de choix possibles dès la saisie des premiers caractères.
2. Mise en place
2.1 Installation de Prototype et Scriptaculous
Pour profiter des automatismes du Helper Ajax fourni avec CakePHP, nous avons besoin du framework javascript Prototype et de son extension Script.aculo.us.
Télécharger Prototype
Télécharger Script.aculo.us
Nous décompressons les archives respectives dans le répertoire {app}/webroot/js.
Nous prenons le soin d’ajouter le Helper Javascript dans l’AppController pour pouvoir inclure facilement les librairies dans le layout général :
1 2 3 4 5 | // {app}/app_controller.php class AppController extends Controller { var $helpers = array ('Html', 'Form', 'Javascript'); } |
Nous pouvons maintenant inclure les deux librairies dans le layout :
1 2 3 4 5 6 7 8 9 | // {app}/views/layout/default.ctp <head> <?php e($html->charset('iso-8859-1')); e($html->css('test', null, array('media' => 'screen'))); e($javascript->link('prototype')); e($javascript->link('scriptaculous.js?load=effects,controls')); ?> </head> |
2.2 Le Modèle Personne
Nous créons la table suivante :
1 2 3 4 5 6 7 | CREATE TABLE `personnes` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY , `nom` VARCHAR( 50 ) NOT NULL , `prenom` VARCHAR( 50 ) NOT NULL , `profession` VARCHAR( 50 ) NOT NULL , `created` DATETIME NOT NULL ); |
Et le modèle associé :
1 2 3 4 5 | // {app}/models/personne.php class Personne extends AppModel { var $name = 'Personne'; } |
2.3 Configuration du Router
La détection des requêtes Ajax par CakePHP peut être gérée automatiquement en ajoutant simplement une ligne dans le Router :
1 2 | // {app}/config/routes.php Router::parseExtensions(); |
Cette instruction, associée à l’utilisation du Composant RequestHandler dans notre Contrôleur PersonnesController, va permettre à CakePHP de prendre en charge le changement du layout lorsqu’il détecte une requête en Ajax.
2.4 Le Layout Ajax
Le layout utilisé par CakePHP pour renvoyer une réponse Ajax se trouve dans {app}/views/layout/ajax.ctp. Si nous l’ouvrons nous constatons qu’il ne fait qu’afficher la variable $content_for_layout, sans rien autour.
Nous allons ajouter une instruction dans ce layout pour afficher correctement les accents que pourrait contenir la réponse Ajax :
1 2 3 | // {app}/views/layout/ajax.ctp <?php header('Content-Type: text/xml; charset=ISO-8859-1'); ?> <?php echo $content_for_layout; ?> |
2.5 Le Contrôleur PersonnesController
Le Contrôleur des personnes contient trois actions :
index: liste des personnes par ordre alphabétiqueedit: ajout ou édition d’une personneautocomplete: recherche les professions déjà saisies (10 maximum) commençant par une chaîne de caractères transmise enPOSTpar Ajax
Le Contrôleur va appeller le Composant RequestHandler pour qu’il se charge du format de réponse (normal ou ajax), ainsi que les Helpers AjaxHelper et TextHelper. Ce dernier va nous servir à mettre facilement en exergue la chaîne de caractères recherchée dans les noms de profession renvoyés par l’autocomplete.
Nous allons également prendre le soin de régler le niveau de debug à 0 s’il s’agit d’une requête Ajax, pour ne pas polluer la réponse avec le dump SQL. Cela se fait dans le callback beforeFilter, où nous testons le retour de la méthode isAjax du RequestHandler.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | class PersonnesController extends AppController { var $name = 'Personnes'; var $helpers = array('Ajax', 'Text'); var $components = array('RequestHandler'); function beforeFilter() { parent::beforeFilter(); if($this->RequestHandler->isAjax()) { Configure::write('debug', 0); } } function index() { $personnes = $this->Personne->find( 'all', array( 'order' => 'nom, prenom' ) ); $this->set(compact('personnes')); } function edit($id = null) { if(isset($this->data)) { $this->Personne->set($this->data); $this->Personne->save(); $this->redirect('index'); } $this->data = $this->Personne->read(null, $id); } function autocomplete() { $recherche = utf8_decode($this->data['Personne']['profession']); $professions = $this->Personne->find( 'all', array( 'fields' => 'DISTINCT profession', 'conditions' => "profession LIKE '$recherche%'", 'order' => 'profession', 'limit' => 10 ) ); $this->set(compact('professions', 'recherche')); } } |
2.6 La Vue edit
Voyons le formulaire d’ajout ou de modification d’une personne, avec le champ profession en autocomplete :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // {app}/views/personnes/edit.ctp <?php e($form->create('Personne', array('action' => 'edit'))); e($form->input('id')); e($form->input('nom', array('label' => "Nom :"))); e($form->input('prenom', array('label' => "Prénom :"))); ?> <div class="input"> <label>Profession :</label> <?php e($ajax->autoComplete( 'Personne.profession', '/personnes/autocomplete', array( 'minChars' => 3, 'indicator' => 'ajaxloader' ) )); ?> <div id="ajaxloader" style="display:none;"> Chargement... </div> </div> <?php e($form->end('Valider')); ?> <p><a href="/personnes">Liste des personnes</a></p> |
Nous utilisons la méthode autoComplete fournie par l’AjaxHelper pour créer le champ profession. Cette méthode prend en paramètres le nom du champ, l’url qui va renvoyer une réponse Ajax, et un tableau d’options, ici le nombre de caractères minimum à saisir avant de déclencher une requête Ajax, et l’identifiant d’un div dans la page servant de témoin d’activité Ajax.
Ce div, d’identifiant “ajaxloader”, est caché par défaut, et sera affiché par Prototype tant qu’une requête Ajax est en cours.
2.7 La Vue Ajax autocomplete
Nous finissons avec la vue qui va servir à mettre en forme la réponse Ajax de l’action autocomplete : il s’agit d’une simple liste non ordonnée des noms de profession renvoyés.
Nous utilisons la méthode highlight du TextHelper pour entourer la chaîne de caractères recherchée par les balises de mise en gras. Ces balises disparaîtront automatiquement lors du clic sur l’un des noms de profession et ne serons donc pas sauvegardées.
1 2 3 4 5 6 7 8 9 10 | // {app}/views/personnes/autocomplete.ctp <ul> <?php foreach($professions as $profession): ?> <li><?php e($text->highlight( $profession['Personne']['profession'], $recherche, '<strong>\1</strong>' )); ?></li> <?php endforeach; ?> </ul> |
Embellissons un peu notre champ autocomplete avec les quelques règles CSS suivantes (les noms des classes sont imposés par l’AjaxHelper) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // {app}/webroot/css/test.css .auto_complete { position: absolute; background: white; border: 1px solid #ccc; } .auto_complete ul { list-style: none; margin: 0; padding: 0; } .auto_complete ul li { padding: 5px; } .selected { background-color: #ffc; } |
Résultat :

Commentaires
11 avril 2008 à 9:55
Une petite question : quand on télécharge srciptaculous, il y a déjà la librairie prototype dedans, est-ce tout de même nécessaire de l’avoir en dehors en plus ?
De même, le chemin indiqué au point 2.1 pour l’inclusion du script ’scriptaculous.js?load=effects,controls’
n’est pas bon si on suit ton exemple : la décompression de la lib scriptaculous dans webroot/js crée des sous-répertoires et le script concerné n’est donc pas à la racine de /js, mais dans /js/scriptaculous/src/ !
11 avril 2008 à 11:47
Dernières questions :
1) j’ai besoin de l’ID au lieu du Name, y a-t-il une solution pour afficher le nom dans la zone d’autocomplétion, mais transmettre l’ID ? Ou bien devrais-je faire une requête à partir du nom saisi pour retrouver l’ID ?
2) j’ai besoin de 10 champs autocomplétés, mais mon test affiche toujours les mêmes choix d’autocomplétion, même si je commence ma saisie par d’autres lettres, d’où peut venir ce souci ?
11 avril 2008 à 11:49
Effectivement, je préfère télécharger la dernière version de Prototype sur son site officiel, puis compléter avec uniquement les fichiers nécessaires de Scriptaculous, le tout à la racine du répertoire webroot/js.
Mais on peut aller plus vite comme tu le proposes et ne télécharger que Scriptaculous et conserver l’arborescence de l’archive.
11 avril 2008 à 11:54
Ok, alors tu devrais peut-être préciser cela dans l’introduction
11 avril 2008 à 12:05
1) A première vue je ne vois pas cela possible, dans la mesure où l’on peut toujours outrepasser l’autocomplétion. Dans ce cas tu n’auras pas l’id, quelle que soit la méthode de transmission.
2) si les choix sont les mêmes quelles que soient les lettres que tu tapes, ça doit être une erreur au niveau de la requête SQL. Remet le debug à 2 le temps de voir où ça cloche.
11 avril 2008 à 12:55
1) Ok, merci, cela me semble logique aussi. Donc je vais devoir faire une petite requête en beforeSave() !
2) J’ai remis le debug à 2, a priori pas de souci SQL, mais plutôt un problème avec le $recherche = $this->data…
Parceque mes 10 champs représentent 10 fois la même donnée… ce sont des Tags en fait.
Donc dans la méthode autocomplete(), si je fais $this->data['Tag'], je tourne en rond !
Comment faire pour modifier $recherche = $this->data['Tag'] ?
3) Que se passe-t-il en cas d’édition ?
11 avril 2008 à 13:55
En cas d’édition, tout se passe comme si c’était un champ input text habituel.
11 avril 2008 à 14:24
Merci pour ton aide Pierre-Emmanuel !
J’ai trouvé une astuce pour mes dix champs : je passe un paramètre ($id) à la méthode autocomplete() de mon contrôleur, ainsi, j’ai quelque chose comme : $recherche = $this->data[’Tag$id’]
Idem pour le div “ajaxloader”, je lui ajoute un index à son DOM id, sinon, le div s’affichait toujours sous le label du premier champ.
Par contre, en cas d’édition, je ne peux pas bénéficier des méthodes automagiques de Cake, notamment le remplissage de mes champs (car Articel HABTM Tag) en mode édition…
14 avril 2008 à 15:08
Je reviens sur la problématique du mode “Edition” : comment je fais pour passer à la méthode ajax->autocomplete(), l’attribut “value” de mon champ texte ?
15 avril 2008 à 10:14
J’ai trouvé ! Je me doutais bien qu’on pouvait ajouter “value”=>$value dans le tableau passé en 3e paramètre de $ajax->autoComplete(), mais comme je ne passais pas la bonne valeur pour $value, j’avais l’impression que cela ne fonctionnait pas !
Merci pour ce tutoriel, une fois encore, bien rédigé et très pratique pour mettre en place un système rapide d’Autocomplétion !
23 avril 2008 à 9:24
Parfois, l’autocomplétion ne me remonte pas les bons mots : si je tape “Bat” et bien il me remonte les premiers tags par ordre alphabétique “Abr”.
Ce comportement ne se produit pas toujours, parfois c’est directement lorsqu’on arrive sur la page, parfois c’est quand on passe au 2e champ à auto-remplir… C’est comme si j’avais quelque part en session un tableau de mes 10 premiers tags et qu’il me ressorte cette valeur en mémoire.
As-tu remarqué ce genre de bug et as-tu une piste ?
23 avril 2008 à 10:07
Je n’ai jamais eu ce problème, même sur un formulaire avec plusieurs champs autocomplete. Commence par tracer les requêtes SQL qui sont exécutées, puis analyse les aller-retour Ajax avec FireBug.
12 juin 2008 à 12:08
Merci pour ce tuto détaillé, et en général pour l’ensemble de ton oeuvre sur ce site
24 juin 2008 à 11:26
Bonjour, j’ai essayer d’appliquer exactement votre exemple en créant la table personne, les controller et tout ce qui suit. Mais le résultat ne s’affiche pas. En effet j’obtient un message indiquant que le chargement de l’info est en cours, mais rien ne s’affiche. J’ai pourtant rempli la table avec des profession commençant par les lettres que je met dans la liste déroulante.
24 juin 2008 à 15:05
Commencez par voir si la recherche fonctionne en appelant l’action directement dans votre navigateur (sans passer par l’ajax).
25 juin 2008 à 16:48
En appelant simplement la méthode autocomplate() dans mon code , j’arrive à afficher le résultat directement dans le navigateur. Mais cela ne s’effectue pas avec le code ajax.
25 juin 2008 à 16:51
En fait j’ai afficher le tableau de résultat grace à print_r(); dans la méthode autocomplete(); en fournissant une valeur quelconque à la variable $recherche et j’ai appelé cette méthode dans la méthode edit() en faisant $this->autocomplete();
11 juillet 2008 à 18:10
Bonjour, moi aussi j’ai exactement le meme probleme !
En fait, tout se passe très bien, le resultat est meme renvoyé et ajouté dans le DIV. Seulement, celui-ci garde la propriété display:none, et donc il n’est pas affiché.
Je n’ai vraiment trouvé aucune solution, à part un mec sur un forum qui avait le meme probleme, mais qui a finalement edité son message en disant que finalement ca marchait, du jour au lendemain.
Donc si quelqu’un a une idée…