Une problématique qui s’est posée à moi dans un développement récemment était l’envoi d’un formulaire en ajax avec Symfony 4 (fonctionne aussi bien avec Symfony 3).
Contexte
Côté serveur, l’application en question tourne sous Symfony 4.1. En front, pas d’exotisme, j’utilise Twig auquel j’ajoute Bootstrap 4 ainsi que jQuery. Le cas que je vais présenter en exemple est simple : j’affiche une page qui me liste les articles existants, dans cette page, je ferai apparaître une fenêtre modale de création d’article contenant un formulaire qui sera soumis en ajax. Le but est de rester dans la page principale, malgré l’envoi et le traitement du formulaire.
Entité
Une entité relativement basique, nous mettons juste une contrainte d’intégrité sur mes champs pour éviter qu’ils soient vides et ainsi tester que le formulaire est bien validé par Symfony.
Pour la créer :
php bin/console make:entity Article
L’assistant va me demander quels champs nous souhaitons créer, nous allons créer simplement un champ title (tous les paramètres par défaut) et un champ text (il suffit juste de préciser le type text, tous les autres paramètres également par défaut).
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; /** * @ORM\Entity(repositoryClass="App\Repository\ArticleRepository") */ class Article { /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string", length=255) * @Assert\NotBlank() */ private $title; /** * @ORM\Column(type="text") * @Assert\NotBlank() */ private $text; public function getId() { return $this->id; } public function getTitle(): ?string { return $this->title; } public function setTitle(string $title): self { $this->title = $title; return $this; } public function getText(): ?string { return $this->text; } public function setText(string $text): self { $this->text = $text; return $this; } }
Formulaire
Pour le formulaire, idem, que du basique, j’utilise la commande console pour gagner du temps.
php bin/console make:form ArticleForm
La commande va me demander l’entité associée, nous précisons donc Article.
Nous allons légèrement modifier le code généré afin de retirer le caractère obligatoire des champs en HTML5. Attention, ce n’est pas à faire en temps normal, mais de cette façon, nous allons pouvoir nous assurer que le formulaire est bien géré et validé correctement en ajax un peu plus bas.
<?php namespace App\Form; use App\Entity\Article; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; class ArticleType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('title', TextType::class, array( 'required' => false, )) ->add('text', TextareaType::class, array( 'required' => false, )) ; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ 'data_class' => Article::class, ]); } }
Controller
Concernant le controller, comme d’hab, nous le générons avec la console :
php bin/console make:controller ArticleController
La commande me génère donc notre controller avec une action index, ainsi qu’un template article/index.html.twig.
Nous allons modifier l’action par défaut afin de récupérer la liste des articles existants.
// ... /** * @Route("/article", name="article") */ public function index() { $repository = $this->getDoctrine()->getRepository(Article::class); $articles = $repository->findAll(); return $this->render('article/index.html.twig', [ 'articles' => $articles, ]); }
Nous allons également créer une action pour la création de notre article, elle est très similaire à ce que vous pouvez connaître dans un CRUD classique avec Symfony, si ce n’est qu’au lieu de rediriger vers une autre action après l’enregistrement, on renvoie simplement une réponse qui va indiquer que l’enregistrement a été effectué. Il y a différentes manières de le faire, nous faisons au plus simple en renvoyant simplement un objet Response() qui contiendra le message « success ».
En annotation, vous remarquerez également le paramètre condition= »request.isXmlHttpRequest() » qui permet de limiter les appels à cette action aux seuls appels en ajax.
Au niveau du formulaire, nous allons également préciser le paramètre action afin qu’il retourne vers la même action du controller. En effet, comme cette action sera appelée depuis une autre action en ajax, le formulaire renverra vers l’index du controller qui ne sait pas comment gérer le formulaire.
Voici le code :
// ... /** * @Route("/article/create", name="article_create", condition="request.isXmlHttpRequest()") */ public function create(Request $request) { $article = new Article(); $form = $this->createForm(ArticleType::class, $article, array( 'action' => $this->generateUrl($request->get('_route')) )) ->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $this->getDoctrine()->getManager()->persist($article); $this->getDoctrine()->getManager()->flush(); return new Response('success'); } return $this->render('article/_create.html.twig', [ 'form' => $form->createView(), ]); }
Templates
Ils sont au nombre de 3, mais seulement 2 vont réellement nous intéresser.
Lors de la génération du controller et de l’action index, un template index.html.twig est créé, mais également un template base.html.twig dont index.html.twig hérite. Tout est réalisé automatiquement par la commande console citée plus haut. Nous ne toucherons pas au base.html.twig.
Nous allons d’abord voir le formulaire en lui même. Il nous faut créer un template que nous allons nommer _create.html.twig dans le répertoire templates/article.
Ce template est encore une fois minimaliste, il ne contient que le formulaire de création de l’article.
{{ form_start(form) }} {{ form_widget(form) }} <div class="form-group row"> <div class="col-sm-2"></div> <div class="col-sm-10"> <button type="submit" class="btn btn-primary" data-label="Enregistrer"> Enregistrer </button> </div> </div> {{ form_end(form) }}
En revanche, dans index.html.twig, il y a un certain nombre de modification à réaliser. Il faut appeler les différents styles et scripts dont on aura besoin. Il faut également créer la fenêtre modale (grâce à bootstrap) ainsi que le bouton qui affichera cette modale. Et bien entendu le code javascript où tout va réellement se passer.
Il y a 2 parties à ce code javascript : la première est responsable de l’affichage de la fenêtre modale, et la seconde sera un peu plus chargée, il s’agit de la soumission du formulaire.
Pour la première partie, c’est très classique, un simple appel ajax à l’action article_create de notre controller pour afficher le formulaire dans la fenêtre modale.
Pour la 2ème partie c’est un peu plus compliqué. Je précise que le code présenté ici n’est qu’une proposition, il y a des quantités de manières différentes d’arriver au même résultat. La première chose à faire est de court-circuiter la soumission normale du formulaire afin de la gérer en javascript. Vous remarquerez que l’appel ajax pour la soumission du formulaire est légèrement différent de celui utilisé pour l’affichage de la fenêtre. En fait je n’utilise pas là directement jQuery, mais un plugin du nom de jQuery form. Il permet de simplifier la soumission en ajax et la serialization des données du formulaire. Je vous invite à y jeter un coup d’oeil parce qu’il est très pratique. Pour être honnête, on aurait pu s’en passer dans cet exemple, il aurait alors fallut serializer les données du formulaire en faisant $form.serialize(). Simple me direz-vous, alors pourquoi ne pas faire ça au lieu de passer par un plugin ? Et bien si vous aviez eu un champ fichier dans votre formulaire, la serialization de ce champ n’aurait pas fonctionné et le champ n’aurait pas été envoyé avec le reste du formulaire.
Je vous invite à regarder plus particulièrement la section success de l’appel ajax. Il y a 2 cas que nous gérons ici, le cas où l’enregistrement s’effectue bien (data == ‘success’) et quand ce n’est pas le cas (et donc que la validation du formulaire ne passe pas). Dans ce 2ème cas de figure, notre controller nous renvoie le formulaire avec ses messages d’erreur. Vous pouvez d’ailleurs faire le test en envoyant le formulaire vide, vous verrez les messages d’erreurs s’afficher correctement.
Voici donc le code de notre template :
{% extends 'base.html.twig' %} {% block stylesheets %} <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" crossorigin="anonymous"> {% endblock %} {% block body %} <div> <h1>Mes articles</h1> <button class="btn btn-primary" data-toggle="modal" data-target="#exampleModal"> <i class="fa fa-plus"></i> Ajouter un article </button> <ul> {% for article in articles %} <li>{{ article.title }}</li> {% endfor %} </ul> </div> <!-- Modal --> <div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalTitle" aria-hidden="true"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="exampleModalTitle">Ajout d'un article</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"></div> </div> </div> </div> {% endblock %} {% block javascripts %} {{ parent() }} <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/js/bootstrap.min.js" integrity="sha384-uefMccjFJAIv6A+rW+L4AHf99KvxDjWSu1z9VI8SKNVmz4sk7buKt/6v9KI65qnm" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.form/4.2.2/jquery.form.min.js"></script> <script> $('#exampleModal').on('shown.bs.modal', function () { var modal = $(this); $.ajax('{{ path('article_create') }}', { success: function (data) { modal.find('.modal-body').html(data); } }); ); $(document).on('submit', 'form', function (e) { // il est impératif de commencer avec cette méthode qui va empêcher le navigateur d'envoyer le formulaire lui-même e.preventDefault(); $form = $(e.target); modal = $('#exampleModal'); var title = $('#article_title').val(); var $submitButton = $form.find(':submit'); $submitButton.html('<i class="fas fa-spinner fa-pulse"></i>'); $submitButton.prop('disabled', true); // ajaxSubmit du plugin ajaxForm nécessaire pour l'upload de fichier $form.ajaxSubmit({ type: 'post', success: function (data) { if (data == 'ok') { $('ul').append('<li>' + title + '</li>'); modal.modal('toggle'); } else { modal.find('.modal-body').html(data); } }, error: function (jqXHR, status, error) { $submitButton.html(button.data('label')); $submitButton.prop('disabled', false); } }); }); </script> {% endblock %}
Vous avez donc tout ce qu’il faut pour tester notre exemple, une fois que le formulaire validé, la fenêtre va disparaître et le titre de l’article sera affiché dans la liste de la page.
Conclusion
L’envoi de formulaire en ajax avec Symfony n’est pas sorcier une fois qu’on sait comment conjuguer simplement les différents éléments qui doivent entrer en ligne de compte.
Il est également possible de serializer le formulaire avec FormData sur les navigateurs récents, mais l’utilisation du plugin cité un peu plus haut pour cet usage peut rendre la compatibilité de votre code un peu plus large.