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 $contentforlayout, 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étique
  • edit : ajout ou édition d’une personne
  • autocomplete : recherche les professions déjà saisies (10 maximum) commençant par une chaîne de caractères transmise en POST par 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 :

Autocomplete en Ajax

Pierre-Emmanuel Fringant

Articles connexes

Commentaires

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/ !

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 ?

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.

Ok, alors tu devrais peut-être préciser cela dans l’introduction

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.

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 ?

En cas d’édition, tout se passe comme si c’était un champ input text habituel.

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…

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 ?

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 !

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 ?

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.

Merci pour ce tuto détaillé, et en général pour l’ensemble de ton oeuvre sur ce site :)

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.

Commencez par voir si la recherche fonctionne en appelant l’action directement dans votre navigateur (sans passer par l’ajax).

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.

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();

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…

Super ce tuto. Tout fonctionne nickel! Merci à vous

Bonjour,

J’ai suivi ce tutorial sur mon projet et ça fonctionne directement, c’est top d’avoir ce genre de tutorial.

Par contre j’aimerai compliqué encore un peu le problème.

J’aimerai que l’autocomplétation se fasse en fonction de la saisie (logique) mais également en fonction d’un autre champ de mon formulaire.

En partant de ton exemple, on pourrait avoir un champ combo ‘Secteur d’activité’ avec ‘Information’, ‘Restauration’, ‘Fonction publique’. Ensuite si ce champ est rempli, la recherche se fera avec les premières lettres saisies mais également en fonction du secteur d’activité.

Comment peut on faire ?

Je viens de trouver. Ca fait maintenant 1h30 que je suis dessus et je viens de trouver la solution.

Juste avant l’envoie de la commande AJAX, le script Autocompleter génère l’url avec les paramètres.

Lors de cette génération, il est possible d’ajouter soit même 2 types de paramètres : - Statique : En utilisant l’option ‘parameters’, - Dynamique : En ajouter la fonction javascript ‘Callback’ dans les options.

Avec ces 2 méthodes, il est possible de faire ce qu’on a envie.

Attention pour que les données passe dans les « datas » du controller, il faut utiliser la syntaxe suivante pour chaque paramètre. data[votremodel'][nomduchamp]=2 ou data[nomdelavariable]=2

j’ai exactement le meme pb que mbainakine. Tout fonctionne mais le resultat ne s’affiche pas dans le tableau. Dans firebug, jai bien un appel au serveur, qui me retourne une reponse HTML qui contient bien la liste des resultats. Mais le souci cest que ces resultats ne sont pas affichés.

Bonjour, j’ai également le problème du display : none. Comment faire pour que autocompleter ne passe pas le display a none a la perte du focus? car meme sur le onblur, en mettant le display a block, cela ne fonctionne pas :s Merci pour votre aide

Je voudrais savoir une chose : est-il possible d’implémenter 2 champs d’auto-complétion sur la même page avec ce script là ?

j’ai aussi le même problème que mbainakine. Quelqu’un aurait-il une solution ? Merci d’avance !

j’ai trouvé d’où venait le problème en ce qui me concerne. L’exemple fournit donne l’url « /personnes/autocomplete » à la méthode autoComplete de l’helper ajax. Ayant travaillé en local sur ma machine en « /caketest/personne/autocomplete », j’ai cru bon de mettre « /caketest/personnes/autocomplete » au lieu de « /personnes/autocomplete ». ERREUR ! Comme tout helper qui se respecte, ce n’était pas nécessaire. En remettant « /personnes/autocomplete » tout marche nickel. J’ai trouvé le pb grâce à la console « réseau » de Firebug sous firefox (si ça peut aider).

Bonjour et merci pour ce tuto très sympa! Il fonctionne très bien, mais j’aimerais aller plus loin et faire une soumission du formulaire lors du clic sur l’un des éléments de la liste. Un onclick= »….submit() » sur la balise

  • ne me garde que les caractères tapés dans le champ (normal), et non tout le terme sélectionné dans la liste, comment pourrais-je faire pour que le submit intervienne après l’inclusion de la valeur
  • dans le champ? Merci beaucoup!
  • J’ai également le problème du display:none qui ne se met pas à jour et empêche de visualiser les résultats.

    Bonjour à tous,

    Super tuto.

    Par contre, petit souci d’intégration avec IE8 ou la liste n’apparait qu’après une seconde frappe sur le même input non rafraichit…une idée ?

    @Quentin,

    Oui, il est possible d’implémenter 2 ou plusieurs champs autocomplete du moment que tes méthodes autocomplete soient présentes dans ton controlleur et que test vues associées soient définies.

    Par contre, il sera nécessaire de renommer differemment le div « ajaxloading »

    Bonjour, Merci pour ton code, il marche bien en gros, mais j’ai deux problèmes :

    1- Le input que ce code donne est un champ de type text, alors que moi j’ai besoin d’un textarea de 300 caractère, et je n’arrive pas a définir la taille de mon champ si ce n’est seulement passé les paramétre width et height à l’id du champ dans mon CSS, ça résou seulement le probléme de taille mais pas de retoure à la ligne!!!

    2- ça m’affiche les informations sans problème, mais dés qu’il y a un caractère spéciale, il ne l’affiche pas.

    Des idées ?

    Bonjour ,

    merci pour ce tuto, il est clair et facile à appliquer.

    Cependant il y a quelque chose qui est assez desagreable : La div qui apparait qui contient nos suggestions se voit imposé un style que j’ai récupéré avec firebug

    style= »position: absolute; left: XXpx; top: YYpx; width: ZZpx; display: none; »

    il s’avére que les valeurs « XX » et « YY » soient pertinente car la div est bien placé sous notre input, cependant la valeur du width elle est trés génante car plus grande que notre input ( dailleurs cela se voit un peu sur ton snapshot ;) )

    Ayant des input assez gros cela se voit énormément sur mon appli est cest horriblement moche. J’ai essayer de passé une clef style dans mon troisieme argument de $ajax->autocomplete() mais cela influe sur l’input et non pas sur la div de suggestion qui se voit encore toujours un peu plus grande.

    Si tu as une idée je suis preneur.

    ps : « (les noms des classes sont imposés par l’AjaxHelper) : » Cela devait surement être vrai au moment ou tu as rédigé ton article, mais à présent en précisant une clef ‘class’ dans le tableau d’option du AjaxHelper tu peux décidé ta class :)

    Bonjour, Je ne sais pas si c’est toujours d’actualité mais j’ai trouvé comment résoudre le problème d’affichage. Il faut juste supprimer dans autocomplete.ctp la variable recherche :

         <li>highlight(
            $nomPers['Personne']['nomPers'],
            //$recherche,
            '<strong>\1</strong>'
         )); ?&gt;</li>

    Ensuite les noms s’affichent.

    Participez

    Pour insérer une portion de code, utilisez <pre lang="php">...</pre>