Gestion des messages de succès ou échec (messages flash) - MTES-MCT/histologe GitHub Wiki
Les messages de succès ou d'échec à afficher suite a une action qui recharge la page fonctionne selon le système traditionnels des messages flash du framework Symfony : Deux possibilités :
- envoie d'un message simple :
$this->addFlash('error', 'Veuillez sélectionner un format pour l\'export.');- envoie d'un message composé d'un titre et du message :
$this->addFlash('success', ['title' => 'Étiquette ajoutée', 'message' => 'L\'étiquette a bien été ajoutée.']);Le template d'affichage de ces message est le fichier flash-messages.html.twig
Pour les retours d'actions sans rechargement de page il existe un système javascript permettant d'afficher les message de succès ou d'échec. Pour cela le controller appelé doit retourner un tableau JSON indiquant le comportement à effectuer. Tous les éléments du tableau JSON sont facultatifs, ci-dessous une documentation par l'exemple, du cas le plus simple au plus complexe.
Les fichiers gérant le comportement js sont component_json_response_handler.js et ajax_form_handler.js
Exemple : Action "Télécharger le PDF" du menu action de la fiche signalement.
Premièrement on ajoute la classe css simple-ajax-link sur le lien
<a href="{{ path('back_signalement_gen_pdf',{uuid:signalement.uuid}) }}"
class="fr-nav__link fr-btn--icon-left fr-icon-file-download-line simple-ajax-link"
title="Télécharger le PDF">
Télécharger le PDF
</a>Ensuite on modifie la réponse du controller pour retourner le tableau JSON. Ici on retourne l'indication stayOnPage => true pour indiquer de ne pas recharger la page et un tableau de message flash, chaque message flash doit contenir un type, un title et un message
$flashMessages[] = ['type' => 'success', 'title' => 'Export envoyé', 'message' => $message];
return $this->json(['stayOnPage' => true, 'flashMessages' => $flashMessages]);Exemple : Action "Envoyer le lien de suivi" du menu action de la fiche signalement.
Premièrement on s'assure que la modale soit englobé dans un <div data-ajax-form> afin qu'elle soit soumise en ajax
<div data-ajax-form>
{% include '_partials/_modal_send_lien_suivi.html.twig' %}
</div>Ensuite on modifie la réponse du controller pour retourner le tableau JSON. Ici on retourne l'indication de ne pas recharger la page, le message de succès, et l'indication de refermer la modale.
$flashMessages[] = ['type' => 'success', 'title' => 'Lien de suivi envoyé', 'message' => 'Le lien de suivi a été envoyé par e-mail.'];
return $this->json(['stayOnPage' => true, 'flashMessages' => $flashMessages, 'closeModal' => true]);En cas d'erreur on retournerait la même chose mais avec un message de type alert et sans fermeture de la modale.
$flashMessages[] = ['type' => 'alert', 'title' => 'Erreur', 'message' => 'Le lien de suivi n\'a pas pu être envoyé par e-mail.'];
return $this->json(['stayOnPage' => true, 'flashMessages' => $flashMessages]);Exemple : Soumission de la modale "Modifier les coordonnées du foyer" de la fiche signalement
Comme toujours la modale doit être englobé dans un <div data-ajax-form> afin qu'elle soit soumise en ajax
<div data-ajax-form>
{% include 'back/signalement/view/edit-modals/edit-coordonnees-foyer.html.twig' %}
</div>Ensuite on effectue un travail préparatoire pour séparer en template distincts les parties de la page contenant les données qui vont changer suite à notre action.
Ici il s'agit de back/signalement/view/header/_title.html.twig et back/signalement/view/information/information-foyer.html.twig on les englobe dans un id css que l'on pourra ciblé.
<div id="signalement-title-container">
{% include 'back/signalement/view/header/_title.html.twig' %}
</div><div id="signalement-information-foyer-container">
{% include 'back/signalement/view/information/information-foyer.html.twig' %}
</div>Enfin on modifie la réponse du controller pour retourner le tableau JSON. Ici on retourne l'indication de ne pas recharger la page, le message de succès, l'indication de refermer la modale et le nouveau contenu html des template préalablement séparés est ciblés.
$flashMessages[] = ['type' => 'success', 'title' => 'Modifications enregistrées', 'message' => 'Les coordonnées du foyer ont bien été modifiées.'];
$htmlTargetContents = [
[
'target' => '#signalement-title-container',
'content' => $this->renderView('back/signalement/view/header/_title.html.twig', ['signalement' => $signalement]),
],
[
'target' => '#signalement-information-foyer-container',
'content' => $this->renderView('back/signalement/view/information/information-foyer.html.twig', ['signalement' => $signalement]),
],
];
return $this->json(['stayOnPage' => true, 'flashMessages' => $flashMessages, 'closeModal' => true, 'htmlTargetContents' => $htmlTargetContents]);En cas d'erreur il suffit de retourner le message d'erreur avec l'indication de ne pas recharger la page.
$flashMessages[] = ['type' => 'alert', 'title' => 'Erreur', 'message' => 'Le jeton CSRF est invalide. Veuillez actualiser la page et réessayer.'];
return $this->json(['stayOnPage' => true, 'flashMessages' => $flashMessages]);Exemple : Suppression d'une document type
Comme toujours la modale d'action doit être englobé dans un <div data-ajax-form> et contenir une formulaire afin qu'elle soit soumise en ajax
Ensuite on effectue un travail préparatoire pour séparer en template distincts les parties de la page contenant les données qui vont changer suite à notre action.
Ici il s'agit de back/admin-territory-files/_title-list-results.html.twig et back/admin-territory-files/_table-list-results.html.twig on les englobe dans un id css que l'on pourra ciblé.
<section class="fr-col-12 fr-py-5v" id="title-list-results">
{% include 'back/admin-territory-files/_title-list-results.html.twig' %}
</section><section class="fr-col-12 fr-pt-0 fr-px-5v" id="table-list-results">
{% include 'back/admin-territory-files/_table-list-results.html.twig' %}
</section>Dans le formulaire soumis par la modale on ajoute un champ search_params qui contient la liste des paramètre de l'url (recherche, pagination) qui permettront au controller de renvoyer la version du html actualisé avec la prise en compte de ses paramètre initiaux.
On s'assure aussi que le formulaire contient l'attribut data-submit-type="formData" (dans le cas inverse la soumission se fait sous la forme d'un payload json et le paramètre search_params sera ignoré.
<form id="fr-modal-document-delete-form" method="post" action="#" data-submit-type="formData">
<input type="hidden" name="search_params" value="{{ app.request.query.all|url_encode }}">
</form>Afin que, suite à la mise à jour du html les boutons d'actions intéragissant avec javascript continuent de fonctionner on s'assure que les événements les concernant utilise la délégation d'événement.
document.addEventListener('click', (e) => {
if (e.target.closest('.open-modal-document-delete')) {
const button = e.target.closest('.open-modal-document-delete');
document.getElementById('fr-modal-document-delete-document-title').textContent = button.dataset.title;
document.getElementById('fr-modal-document-delete-document-territoire').textContent = button.dataset.territoire;
document.getElementById('fr-modal-document-delete-document-title-reminder').textContent = button.dataset.title;
document.getElementById('fr-modal-document-delete-form').action = button.dataset.url;
}
});Exemple d'une diff de changements apportés pour passer à la délégation d'événements
Dans le controller nous créons une fonction handleSearch qui sera utilisé par l'index et les controler d'action devant retourné le html filtré.
private function handleSearch(Request $request, bool $fromSearchParams = false): array
{
/** @var User $user */
$user = $this->getUser();
$searchTerritoryFiles = new SearchTerritoryFiles($user);
$form = $this->createForm(SearchTerritoryFilesType::class, $searchTerritoryFiles);
FormHelper::handleFormSubmitFromRequestOrSearchParams($form, $request, $fromSearchParams);
if ($form->isSubmitted() && !$form->isValid()) {
$searchTerritoryFiles = new SearchTerritoryFiles($user);
}
$territories = null;
if (!$this->isGranted('ROLE_ADMIN')) {
$territories = $user->getPartnersTerritories();
}
/** @var Paginator $paginatedFiles */
$paginatedFiles = $this->fileRepository->findFilteredPaginated($searchTerritoryFiles, $territories, $this->maxListPagination);
return [$form, $searchTerritoryFiles, $paginatedFiles];
}search_params est FormHelper::handleFormSubmitFromRequestOrSearchParams($form, $request, $fromSearchParams); tout le reste n'est que le traitement classique en provenance du controller index.
Enfin dans le controller de l'action on utilise cette fonction handleSearch avec le second paramètre a true pour renvoyer les bonne données. On renvoi aussi de manière classique les clé indiquant de rester sur la page, le message de succés et la fermeture de la modale
$flashMessages[] = ['type' => 'success', 'title' => 'Document supprimé', 'message' => 'Le document a bien été supprimé.'];
[, $searchTerritoryFiles, $paginatedFiles] = $this->handleSearch($request, true);
$tableListResult = $this->renderView('back/admin-territory-files/_table-list-results.html.twig', [
'searchTerritoryFiles' => $searchTerritoryFiles,
'files' => $paginatedFiles,
'pages' => (int) ceil($paginatedFiles->count() / $this->maxListPagination),
]);
$titleListResult = $this->renderView('back/admin-territory-files/_title-list-results.html.twig', [
'files' => $paginatedFiles,
]);
$htmlTargetContents = [
['target' => '#table-list-results', 'content' => $tableListResult],
['target' => '#title-list-results', 'content' => $titleListResult],
];
return $this->json(['stayOnPage' => true, 'flashMessages' => $flashMessages, 'closeModal' => true, 'htmlTargetContents' => $htmlTargetContents]);Si suite au retour d'une action on doit lancer des actions javascript spécifique (autre que l'affichage des message flash, la fermeture de la modale et qui ne sont pas géré suite à une interaction ou l'on utiliserait la délégation d'événements), la solution préconisé est la suivante :
- Ajouter au tableau JSON de retour une clé
functionscontenant une liste de tableau au format ['name' => 'nomDeLaFonction']
$functions = [['name' => 'applyFilter']];
return $this->json(['stayOnPage' => true, 'flashMessages' => $flashMessages, 'htmlTargetContents' => $htmlTargetContents, 'functions' => $functions]);
```php
- Adapter dans `component_json_response_handler.js` la liste des cas autorisés
```javascript
if(response.functions){
response.functions.forEach((fn) => {
switch(fn.name){
case 'applyFilter':
applyFilter();
break;
}
});
}Ce genre d'usage devrai resté très limité.