Ajout dynamique d’inputs [autocompletion part 2]
Cet article est la suite de celui sur l’autocompletion.
Maintenant que j’ai mon champ texte qui s’autocomplète, j’aimerais bien que lorsque je le remplis un deuxième champ vide apparaisse en dessous, et ainsi de suite.
On va donc créer des inputs dynamiquement avec jQuery. Je ne détaillerai pas l’utilisation de toutes les fonctions jQuery, notamment les fonctions de parcours du DOM, ce n’est pas le but de ce post. Pour une bonne explication en français, je trouve cette documentation très bien.
jQuery propose une fonction très pratique pour ce que l’on veut réaliser ici, c’est la fonction live().
live() va lier une fonction à un évènement pour un ou plusieurs éléments du DOM, qu’ils existent déjà ou pas, à la différence de bind() qui ne s’applique qu’aux éléments pre-existants.
Premièrement, on va différencier tous nos inputs du reste du DOM par une classe. Ajoutons déjà une classe à notre 1er input.
//lib/form/doctrine/BackendModuleForm.class.php $this->widgetSchema['input_auto']->setAttributes(array('class' => 'CM2'));
On peut maintenant utiliser la fonction live() sur cette classe. Et lui lier une fonction, sur l’évenement ‘keyup’ par exemple. Notre fonction va ajouter un input qui possède la même classe ‘CM2‘, la fonction live() s’appliquera donc également au nouvel élement ajouté.
//web/js/add_inputs.js jQuery('.CM2').live('keyup', function() { var myinput = '<div><input id="new" type="text" name="new" autocomplete="off" class="CM2" /></div>'; //Add the input elements to the DOM jQuery(".sf_admin_form_row.sf_admin_text.sf_admin_form_field_input_auto").append(myinput); });
Le fichier précédant est bien sur à inclure dans le template du module concerné pour qu’il soit pris en compte.
<?php use_javascript(‘add_inputs.js’) ?>
ou dans la config view.yml du module :
javascripts: [add_inputs.js]
Qu’a-t-on fait pour l’instant ? Des inputs qui se rajoutent au DOM à chaque fois qu’on tape (évènement keyup) quelque chose dans les champs texte. Ce n’est pas vraiment ce que l’on veut. Un nouvel input doit être créé uniquement si on est en train de remplir de dernier input de la liste. On va donc compter les inputs à chaque appel de la fonction et vérifier si l’utilisateur est en train de taper dans le dernier input.
jQuery('.CM2').live('keyup', function()
{
// fetch all the text input elements
var inputElements = jQuery(".sf_admin_form_row.sf_admin_text.sf_admin_form_field_input_auto input[type=text]");
// count the amount of input elements
var elementCount=inputElements.length * 1; //force type to number
//if we are on the right index (the last one)
if(jQuery(this).parent().prevAll().length == elementCount-1){
//Add the input HERE
}
});
Voilà, c’est mieux. Mais ça pourrait être encore mieux si chaque nouvel input possédait lui aussi la fonction autocomplete. On va donc créer et rajouter un widget sfWidgetFormJQueryAutocompleter à chaque fois qu’on en aura besoin. Comment ? Ajax !
Commençons par le commencement.
Dans la classe formulaire du module, je définie une fonction qui crée et me renvoie le widget dont j’ai besoin.
//lib/form/doctrine/ModuleForm.class.php public function createWidgetAutocompleter() { $auto = new sfWidgetFormJQueryAutocompleter(array( 'url' => "myAction", 'config' => '{ extraParams: { second_parametre: function() { return jQuery("#id_html").val(); } }, scrollHeight: 250 , autoFill: true }')); $auto->setAttributes(array('class' => 'CM2')); return $auto; }
Je vais pouvoir utiliser cette fonction publique dans une action, qui sera appelée en ajax.
Le principe de cette action est donc d’instancier un widget(grâce à la fonction précédente) et de le renvoyer au format Html.
//apps/backend/modules/module/actions/actions.class.php public function executeRenderWidgetAutocompleter(sfWebRequest $request) { $this->getResponse()->setContentType('text/html; charset=utf-8'); $current_id = $request->getParameter('current_id'); $form = new ModuleForm(); $auto = $form->createWidgetAutocompleter(); $html_auto = $auto->render('module[input_auto_'.$current_id.']'); $html = '<label for="module_input_auto_'.$current_id.'"> my input number:(_'.$current_id.')</label>' . $html_auto; return $this->renderText($html); }
La fonction javascript suivante va permettre d’appeler l’action renderWidgetAutocompleter.
On lui passe accésoirement, le compteur d’inputs, pour savoir à combien d’inputs on en est.
On ajoute au DOM, l’html ainsi renvoyé par l’action.
var elementState = 'ready'; var getWidgetAutocompleter = function(count) { if(elementState == 'ready'){ elementState = 'building'; jQuery.ajax({ type: "GET", url: 'renderWidgetAutocompleter', data: { current_id: count }, dataType: 'html', success: function(result){ var html = '<div>'; html += result; html += '</div>'; jQuery(".sf_admin_form_row.sf_admin_text.sf_admin_form_field_input_auto").append(html); jQuery('#autocomplete_module_input_auto_'+count).ready(function() { elementState = 'ready'; }); } }); } };
Je configure le routing.yml selon l’endroit où cette fonctionnalité sera implémentée:
//apps/backendconfig/routing.yml ajax_renderwidgetAutocompleter: url: /:id/renderWidgetAutocompleter param: { module: module, action: renderWidgetAutocompleter }
Je n’ai plus qu’à utiliser ma nouvelle fonction:
//web/js/add_inputs.js jQuery('.CM2').live('keyup', function() { // fetch all the text input elements var inputElements = jQuery(".sf_admin_form_row.sf_admin_text.sf_admin_form_field_input_auto input[type=text]"); // count the amount of input elements var elementCount=inputElements.length * 1; //force type to number //if we are on the right index (the last one) if(jQuery(this).parent().prevAll().length == elementCount-1){ elementCount++; //On appelle notre nouvelle fonction: getWidgetAutocompleter(elementCount); } })
And that’s it !
[...] appears after the first one. And then a third one when you fill in the second on, and so on. (see this previous article (in french)) First, let’s see the layout in the middle of our form [...]
Merci pour le tuto. Mais deux questions :
- Une fois le formulaire envoyé, comment traiter les différentes valeurs en input? (est-on obligé de le faire à la main)
- Quand on modifie notre objet, comment fait-on pour reconstruire tous les objets autocomplete?
En ce qui concerne le traitement des valeurs en input :
On part donc du principe que tu ajoutes ces autocomplete inputs dans ModuleA du ModelA, et ce qui est affiché dans la liste autocomplete vient du ModelB, avec relation Many-to-many entre A et B.
Donc dans ModelAForm.class.php, tu override la méthode saveModelBList(), qui se trouve à l’origine dans BaseModelAForm.class.php pour ignorer le if (!isset($this->widgetSchema['modelB_list']))
Auparavant, tu peux faire un validator qui constate que les valeurs passées dans modelB_list correspondent bien dans ta base de données, histoire que des modifs passées par firebug ne cassent pas tout
En ce qui concerne la reconstruction des inputs, par exemple sur un Edit, je passe pour ma part par un component. Imaginons que ton ModuleA consiste à créer une ligne de transports (comme dans l’exemple de mon post “improving autocompletion”), et que tes autocomplete sont donc des stations. Tu as donc :
public function executeStationsList() { /* * if form has already been submitted * but triggered a validator error, * repopulates fields with taintedValues * (otherwise, user has to refill in all the stations * and this is really tedious) */ if ($this->form->isBound()) { $values = $this->form->getTaintedValues(); // we need to retrieve objects stations to repopulate stations_render $stations_array = array(); foreach ($values['stations_list'] as $index => $station_id) { $stations_array[] = $station_id; } // retrieve, as object, stations that were submitted $this->stations_render = Doctrine::getTable('Station')->getArrayOfStations($stations_array); } else { // retrieve line's stations list on edit action. getObject()->getId() is one transit line's ID $this->stations_render = Doctrine::getTable('Station')->getTransitStationsList($this->form->getObject()->getId()); } }Et puis un partial :
On appelle une fonction createWidgets dans la classe form, la même que celle appelée par ajax.
Tout comme le code ci-dessus, on rempli si le formulaire a été envoyé mais pas sauvegardé (histoire de ne pas perdre 10 inputs)
On a dans le component un $this->stations_render qui stocke nos stations de métro et les réaffiche via le foreach, et puis on ajoute un input vide supplémentaire à la fin.
<div class="sf_admin_form_row sf_admin_text sf_admin_form_field_input_auto"> <?php $auto = $form->createWidgets()?> <?php if($form->getObject()->isNew() && !$form->isBound()):?> <div> <label for="transit_line[input_auto_0]">Station</label> <?php echo $auto->render('transit_line[stations_list][input_auto_0]')?> </div> <?php else:?> <?php $count = 0;?> <?php foreach($stations_render as $station):?> <div> <label for="station_line[input_auto<?php echo $count?>]">Station <?php echo $count?></label> <?php echo $auto->render('transit_line[stations_list][input_auto_'.$count.']', $station->getId(), array('value' => $station->getName()))?> </div> <?php $count++?> <?php endforeach;?> <div> <label for="station_line[input_auto<?php echo $count?>]">Station <?php echo $count?></label> <?php echo $auto->render('transit_line[stations_list][input_auto_' . $count . ']')?> </div> <?php endif;?> </div>J’espère que ces quelques précisions pourrons t’aider.