Yii : Créer un objet lié à la volée lors de la création du parent

J’ai déjà parlé un petit peu de Yii, le framework de développement PHP le plus utilisé du monde mais aussi le moins connu en France malheureusement. Ce framework permet de faire pas mal de choses sympas, que vous soyez développeur confirmé ou wannabe. Ici j’ai envie de parler d’un cas de figure que j’ai rencontré, qui peut paraître trivial mais qui s’est avéré plus tricky que prévu : Créer un nouvel enregistrement à la volée en ajax d’un modèle tier sur un formulaire. Kézaco? Je m’explique…

J’ai un formulaire pour la création d’une fiche moto (toute ressemblance avec un projet existant est  complètement volontaire) dans lequel je veux enregistrer, par exemple, une catégorie de moto. Mais patatra, dans la liste déroulante de mes catégories je n’ai pas celle que je veux créer. Que faire? Abandonner la création de ma fiche moto et les données déjà saisies pour aller créer une nouvelle catégorie, puis revenir sur une nouvelle fiche de création vierge? Mis à part le fait de tout avoir perdu en route, cela fait tout de même deux fenêtres supplémentaires à charger, et d’un point de vue ergonomique on perd l’utilisateur.

Ma solution est simple : ajouter un bouton « + » à coté de ma liste déroulante. Celle-ci va ouvrir un champ de saisie pour enregistrer la nouvelle catégorie, puis à la validation ma liste déroulante va se rafraîchir toute seule comme une grande avec la nouvelle catégorie déjà sélectionnée. Le tout en Ajax bien sûr, et avec twitter bootstrap.

Captures d’écran du rendu :

Comment est-ce possible?

Si vous n’êtes pas familier avec la notion de vue ou de contrôleur, je vous recommande les tutoriels disponibles sur le site officiel de Yii qui sont très complets. Ceci étant, reprenons un peu ce que nous souhaitons faire :

Cliquer sur un bouton du formulaire de création de fiche moto pour ouvrir quelque chose qui nous permette de créer de catégorie. Ce quelque chose, j’ai choisi de l’afficher à l’aide de l’excellent Modal de twiter bootstrap (http://twitter.github.io/bootstrap/javascript.html#modals) pour des raisons d’ergonomie. J’utilise de fait l’extension Yii Bootstrap (http://www.cniska.net/yii-bootstrap) pour m’y aider.

Pour ce quelque chose toujours, j’avais choisi au départ de faire un formulaire dans le formulaire. Seulement le W3C n’aime pas Inception et n’autorise pas de faire cela, donc cela sera un champ texte avec un bouton d’envoi en ajax. Pour des raisons propres à ma logique personnelle j’ai choisi d’héberger la vue affichant ces deux éléments dans une vue de Catégorie, afin de bien séparer les choses. Ceci nous amène à l’arborescence de fichiers :

  • Controleur :
    • CategorieController.php
  • Vues :
    • Moto/_form.php
    • Categorie/_formModal.php

view/moto/_form

Tout d’abord, le formulaire de création de fiche moto. Nous y trouvons les champs suivants :

</pre>
<div class="control-group"><!--?php echo $form--->labelEx($model,'categorie', array('class'=>'control-label')); ?>
<div class="controls">
 <!--?php echo $form--->dropDownList($model,'categorie_id', CHtml::listData($modelCategorie->findAll(), 'id', 'label'), array(
 'prompt'=>'Sélectionner une catégorie',
 )); ?>
 <!--?php $this--->beginWidget('bootstrap.widgets.TbModal', array('id'=>'myModalCategorie', 'htmlOptions'=>array('class'=>'well'))); ?>
 <!--?php echo $this--->renderPartial('/categorie/_formModal', array('modelCategorie'=>$modelCategorie)); ?>
 <!--?php $this--->endWidget(); //TbModal ?>
 <!--?php $this--->widget('bootstrap.widgets.TbButton', array(
 'label'=>'',
 'type'=>'null',
 'icon'=>'icon-plus',
 'htmlOptions'=>array(
 'data-toggle'=>'modal',
 'data-target'=>'#myModalCategorie',
 ),
 )); ?></div>
</div>
<pre>

On y reconnaitra bien entendu l’affichage de la liste déroulante avec le prompt invitant l’utilisateur à sélectionner une catégorie :

<!--?php echo $form--->dropDownList($model,'categorie_id', CHtml::listData($modelCategorie->findAll(), 'id', 'label'), array(
            'prompt'=>'Sélectionner une catégorie',
        )); ?>

Ensuite la partie intéressante. On ouvre le widget Yii Bootstrap TbModal en lui affectant l’if myModalCategorie, on appelle la vue Categorie/_formModal.php, puis on ferme le widget TbModal:

<!--?php $this--->beginWidget('bootstrap.widgets.TbModal', array('id'=>'myModalCategorie', 'htmlOptions'=>array('class'=>'well'))); ?>
       <!--?php echo $this--->renderPartial('/categorie/_formModal'); ?>
<!--?php $this--->endWidget(); //TbModal ?>

Il faut bien sûr un déclencheur pour ouvrir la fenêtre modale :

        <!--?php $this--->widget('bootstrap.widgets.TbButton', array(
            'label'=>'',
            'type'=>'null',
            'icon'=>'icon-plus',
            'htmlOptions'=>array(
                'data-toggle'=>'modal',
                'data-target'=>'#myModalCategorie',
            ),
        )); ?>

Je passe la mise en forme avec l’icône, peu d’intérêt. En revanche on voit bien l’option ‘data-target’ qui cible l’id myModalCategorie du widget TbModal. Jusque là tout va bien, c’est relativement simple non? Hem…

view/categorie/_formModal.php

Ensuite, attaquons le « quelque chose » de tout à l’heure, qui doit nous permettre d’enregistrer une nouvelle catégorie. Tout d’abord la structure HTML/Php. En suivant la structure dictée par twitter bootstrap nous avons :

</pre>
<div class="modal-header"><a class="close" data-dismiss="modal">×</a>
<h4>Nouvelle catégorie</h4>
</div>
<pre>
<!-- modal-header --></pre>
<div class="modal-body">
<fieldset>
<div class="control-group "><label class="control-label required" for="Categorie_ajax">
 Nom <span class="required">*</span>
 </label>
<div class="controls"><input class="span3" id="Categorie_ajax" type="text" maxlength="45" name="Categorie_ajax" size="45" />
 <span class="help-inline error" id="Categorie_ajax_em_" style="display: none;"></span></div>
</div></fieldset>
</div>
<pre>
<!-- modal-body --></pre>
<div class="modal-footer"><!--?php $this--->widget('bootstrap.widgets.TbButton', array(
 'label'=>'Enregistrer',
 'type'=>'primary',
 'htmlOptions'=>array('id'=>'categorieAjaxSend'),
 ))
 ?>
 <!--?php $this--->widget('bootstrap.widgets.TbButton', array('buttonType'=>'reset', 'label'=>'Effacer')); ?></div>
<pre>
<!-- modal-footer -->

Là encore, je ne vais pas détailler la structure HTML de la fenêtre modale, je vous renvois pour cela aux guides twitter bootstrap cités plus haut. Je vais m’intéresser à deux choses :

– les champs de saisie et d’erreur :

<input class="span3" id="Categorie_ajax" type="text" maxlength="45" name="Categorie_ajax" size="45" />
<span class="help-inline error" id="Categorie_ajax_em_" style="display: none;"></span>

– le bouton d’envoi :

<!--?php $this--->widget('bootstrap.widgets.TbButton', array(
        'label'=>'Enregistrer',
        'type'=>'primary',
        'htmlOptions'=>array('id'=>'categorieAjaxSend'),
    ))
?>

Nous vous ne vous trompez pas, ce bouton n’appelle aucune fonction ni contrôleur, et c’est bien volontaire. Notons juste l’id categorieAjaxSend du bouton. Je vais utiliser plus loins cet Id pour déclencher l’envoie des données en jquery. Tout ça parce qu’on ne peut pas faire de formulaire dans un formulaire… Mais où va le monde?
Du coup, on part sur le code javascript associé, que l’on register dans l’application à l’aide de yii :

<!--?php Yii::app()--->clientScript->registerScript('categorieAjaxSend', "
    $('#categorieAjaxSend').click(function(){
        if ( $('#Categorie_ajax').val() == '')
        {
            $('#Categorie_ajax_em_').html('Ce champ ne peut pas être vide');
            $('#Categorie_ajax_em_').show();
        }
        else
        {
            $.ajax({
                    type: 'POST',
                    url: '" . CController::createUrl('categorie/createFromOccasion') . "',
                    data: { labelCategorieAjax: $('#Categorie_ajax').val() }
                })
                .done(function(data) {
                    $('#Occasion_categorie_id').html(data);
                })
                .fail(function() {
                    alert('Une erreur est survenue, merci de bien vouloir essayer de nouveau');
                })
                .always(function() {
                    $('#Categorie_ajax').val('');
                    $('#myModalCategorie').modal('hide');
                    $('#Categorie_ajax_em_').hide();
                });
        }
    });
");
?>

Ici, au click sur le bouton CategorieAjaxSend, on va d’abord vérifier que le champs n’est pas vide. Si il l’est, alors on l’indique dans le champ prévu à cet effet. Sinon, on peut envoyer dans une variable POST au couple Controleur/Action CController::createUrl(‘categorie/createFromOccasion’) les données labelCategorieAjax: $(‘#Categorie_ajax’).val().

Ensuite, on traite la réponse : Si on échoue on affiche un message d’erreur, sinon on rafraîchit la liste déroulante avec le contenu de la réponse.

Enfin, dans tous les cas : on réinitialise le champs d’input pour une utilisation ultérieure, on efface le message d’erreur, puis pin ferme manuellement la fenêtre modale. Pfiou, nous y sommes presque !

controller/categorie/actionCreateFromOccasion

Maintenant, un cou d’oeil à l’action du contrôleur

public function actionCreateFromOccasion()
{
        $model=new Categorie;
        if(isset($_POST['labelCategorieAjax']))
        {
                $model->label=$_POST['labelCategorieAjax'];
                if(!$model->save()) return;
        }
        $data = Categorie::model()->findAll();
        $data=CHtml::listData($data,'id','label');
        echo CHtml::tag('option',array('value'=>''),CHtml::encode('Sélectionner une catégorie'),true);
        foreach($data as $value=>$name)
        {
            echo CHtml::tag('option',
                       array('value'=>$value, 'selected'=>($value==$model->getAttribute('id'))),CHtml::encode($name),true);
        }
}

Nous allons nous y attarder un tout petit peu plus longtemps… Dans cette action, nous commençons par créer un nouvel objet de type Categorie. Puis, nous testons la présence dans la variable POST de la donnée ‘labelCategorieAjax’. Vous vous souvenez, nous l’avions envoyée à l’aide de JQuery.

Si elle est bien présente, alors on va tout simplement setter l’attribut label de mon nouvel objet de type Categorie avec, puis faire un save() dessus en toute sécurité.

Une fois passé ces lignes, si nous traitons la suite c’est que tout s’est bien déroulé. Alors nous commençons par retrouver toutes les catégories de la base de données, avant de l’affecter dans un ListData pour pouvoir l’exploiter avec Yii.

Alors, on peut commencer à faire un premier echo avec l’option prompt, avant d’attaquer une boucle foreach pour echo chacune des valeurs sous la forme d’une liste d’options. La petite subtilité, c’est l’option ‘selected’=>($value==$model->getAttribute(‘id’)) qui permet de rajouter l’attribut SELECTED sur l’enregistrement que nous venons juste d’ajouter.

Et voilà, nous y sommes, le code javascript de la vue _formModal.php va s’occuper de mettre la liste déroulante des catégories à jour. Si vous avez des questions, n’hésitez pas!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *