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é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…

Participez