Cours Laravel 5.5 – ajax

Ajax est une technologie Javascript fort répandue qui permet d’envoyer des requêtes au serveur et de recevoir des réponses sans rechargement de la page. Il est par ce moyen possible de modifier dynamiquement le DOM, donc une partie de la page.

Dans ce chapitre nous allons voir comment mettre en œuvre Ajax avec Laravel en prenant un cas de l’application d’exemple.

Les messages dans l’application

Pour les utilisateurs autres que rédacteurs et administrateurs il y a la possibilité de laisser un message avec un formulaire :

Lorsqu’un administrateur se connecte et va dans le panneau d’administration il a une page pour ces messages :

L’administrateur peut voir les nouveaux messages grâce à la case à cocher « Nouveau », il peut changer le statut en décochant la case.

Il est évident qu’il serait dommage de recharger la page pour ça alors qu’il suffit d’informer le serveur avec une requête Ajax.

Pour améliorer l’ergonomie on a aussi prévu une petite animation le temps de l’échange de requêtes :

Il faut aussi gérer cette animation avec JavaScript.

Mais il faut distinguer deux cas :

  • la case à cocher Nouveau est cochée :

Dans ce cas si on change le statut d’un message il n’est plus nouveau et donc il doit disparaître et du coup ça change la pagination et il faut recharger la page.

  • la case à cocher Nouveau est décochée :

Dans ce cas au changement de statut le message reste affiché et il n’y a rien à faire de spécial si ce n’est gérer l’animation.

La vue des messages

Dans les vues on en a deux qui concernent l’affichage des messages dans l’administration :

Dans la vue table.blade.php on a le code chargé de l’affichage du tableau des messages :

@foreach($contacts as $contact)
<div class="box">
    <div class="box-body table-responsive">
        <table id="contacts" class="table table-striped table-bordered">
            <thead>
            <tr>
                <th>@lang('Name')</th>
                <th>@lang('Email')</th>
                <th>@lang('New')</th>
                <th>@lang('Creation')</th>
                <th></th>
            </tr>
            </thead>
            <tbody>
                <tr>
                    <td>{{ $contact->name }}</td>
                    <td>{{ $contact->email }}</td>
                    <td>
                        <input type="checkbox" name="seen" value="{{ $contact->id }}" {{ is_null($contact->ingoing) ?  'disabled' : 'checked'}}>
                    </td>
                    <td>{{ $contact->created_at->formatLocalized('%c') }}</td>
                    <td><a class="btn btn-danger btn-xs btn-block" href="{{ route('contacts.destroy', [$contact->id]) }}" role="button" title="@lang('Destroy')"><span class="fa fa-remove"></span></a></td>
                </tr>
            </tbody>
        </table>
    </div>
    <!-- /.box-body -->
    <div id="message" class="box-footer">
        {{ $contact->message }}
    </div>
</div>
<!-- /.box -->
@endforeach

On a une boucle pour passer en revue tous les messages :

@foreach($contacts as $contact)
    ...
@endforeach

Voici le code pour les cases à cocher :

<input type="checkbox" name="seen" value="{{ $contact->id }}" {{ is_null($contact->ingoing) ?  'disabled' : 'checked'}}>

La case est cochée si on trouve un enregistrement en relation dans la table ingoings. On a vu cet aspect de l’application avec cette table qui permet de connaître les nouveaux éléments :

Les cases sont distinguées grâce à l’attribut value qui contient l’identifiant du message.

Le JavaScript

Dans la vue index.blade.php on trouve le Javascript pour gérer Ajax :

var contact = (function () {

    var url = '{{ route('contacts.index') }}'
    var swalTitle = '@lang('Really destroy contact ?')'
    var confirmButtonText = '@lang('Yes')'
    var cancelButtonText = '@lang('No')'
    var errorAjax = '@lang('Looks like there is a server issue...')'

    var onReady = function () {
        $('#pagination').on('click', 'ul.pagination a', function (event) {
            back.pagination(event, $(this), errorAjax)
        })
        $('#pannel').on('change', ':checkbox[name="seen"]', function () {
                back.seen(url, $(this), errorAjax)
            })
            .on('click', 'td a.btn-danger', function (event) {
                back.destroy(event, $(this), url, swalTitle, confirmButtonText, cancelButtonText, errorAjax)
            })
        $('.box-header :radio, .box-header :checkbox').click(function () {
            back.filters(url, errorAjax)
        })
    }

    return {
        onReady: onReady
    }

})();

$(document).ready(contact.onReady)

Dans l’application le Javascript est organisé de façon modulaire. Les variables et les fonctions sont privées et on expose selon les besoins avec une API.

On détecte les changements sur les cases à cocher qui s’appellent seen :

$('#pannel').on('change', ':checkbox[name="seen"]', function () {

On appelle alors une méthode :

back.seen(url, $(this), errorAjax)

La valeur de l’url est donnée par cette ligne de code :

var url = '{{ route('contacts.index') }}'

Donc admin/contacts.

Pour l’application l’essentiel du Javascript pour l’administration est rassemblé dans le fichier adminlte/js/back.js, on trouve donc son chargement dans la vue :

<script src="/adminlte/js/back.js"></script>

On trouve dans ce fichier la méthode seen :

var seen = function (url, that, errorAjax) {
    var urlSeen = url + '/seen/' + that.val()
    // If "new" is checked we must reload the page
    if(getCheckboxValueByName('new')) {
        ajax(urlSeen, 'PUT', url, errorAjax)
    } else {
        ajaxNoLoad(urlSeen, 'PUT', errorAjax, that)
    }
}

On trouve les deux cas évoqués ci-dessus selon l’état de la case à cocher Nouveau.

Si elle est cochée il est fait appel à la méthode ajax :

var ajax = function (target, verb, url, errorAjax) {
    spin()
    $.ajax({
        url: target,
        type: verb
    })
        .done(function () {
            load(url, errorAjax)
        })
        .fail(function () {
            fail(errorAjax)
        }
    )
}

On voit qu’ici au retour on recharge la page :

load(url, errorAjax)

Si elle est décochée on fait appel à la méthode à la méthode ajaxNoLoad :

var ajaxNoLoad = function (target, verb, errorAjax, that) {
    spin()
    $.ajax({
        url: target,
        type: verb
    })
        .done(function () {
            unSpin()
            that.prop('disabled', true)
        })
        .fail(function () {
            fail(errorAjax)
        })
}

Ici au retour on :

  • arrête l’animation (unSpin)
  • rend la case à cocher inactive (that.prop(‘disabled’, true)), on ne peut donc pas revenir au statut Nouveau.

La protection CSRF

Je vous ai dit plusieurs fois dans ce cours que Laravel met en place systématiquement la protection CSRF. Or ici à aucun moment on ne transmet le jeton (token) pour cela !

Si vous regardez dans les headers lors de l’envoi de la requête vous allez trouver ceci :

X-CSRF-TOKEN:nW57unWQZBJGWaaQ4TyKaVcQsRWYo1UMNbKhxj7u

Le middleware de Laravel qui assure la protection (VerifyCsrfToken) ne se contente pas de chercher le jeton dans les paramètres de la requête, il va aussi voir dans les headers s’il y a une information X-CSRF-TOKEN. Ce qui est notre cas.

Mais comment s’est créée cette information ?

Si vous regardez le fichier adminlte/js/back.js vous allez trouver ce code :

$.ajaxSetup({
    headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
    }
})

Le jeton (token) est mémorisé dans les metas :

<meta name="csrf-token" content="{{ csrf_token() }}">

Avec la méthode ajaxSetup on demande à jQuery d’ajouter automatiquement l’information dans les headers. On n’a donc plus à s’en préoccuper ensuite…

Le traitement côté serveur

L’url de la requête est de la forme admin/contacts/seen/n. Avec n représentant l’identifiant du message.

Voici la partie concernée du contrôleur Back/ContactController :

public function updateSeen(Contact $contact)
{
    $contact->ingoing->delete ();

    return response ()->json ();
}

La requête arrive dans la méthode updateSeen. La liaison implicite permet d’obtenir directement une instance du modèle dans la variable $contact.

On peut alors supprimer l’enregistrement en relation dans la table ingoings :

$contact->ingoing->delete ();

Il suffit ensuite de renvoyer une réponse JSON sans information spécifique :

return response ()->json ();

Les commentaires

On a déjà vu dans un précédent chapitre qu’on limite le nombre de commentaires affichés pour les articles et que s’il y en a d’autres on affiche un bouton :

On a déjà les conditions d’affichage de ce bouton et on va maintenant s’intéresser à son action.

Voici le code du bouton :

<a id="nextcomments" href="{{ route('posts.comments', [$post->id, 1]) }}" class="button">@lang('More comments')</a>

Voici la route concernée :

On a deux paramètres qui sont renseignés avec la méthode route. On se retrouve donc avec une url dans ce genre :

...posts/2/comments/1

Voici le Javascript :

$('#nextcomments').click (function(event) {
    event.preventDefault()
    $('#morebutton').hide()
    $('#moreicon').show()
    $.get($(this).attr('href'))
    .done(function(data) {
        $('ol.commentlist').append(data.html)
        if(data.href !== 'none') {
            $('#nextcomments').attr('href', data.href)
            $('#morebutton').show()
        }
        $('#moreicon').hide()
    })
})

Sans entrer dans tous les détails on voit qu’on utilise la méthode $.get de jQuery pour lancer la requête :

$.get($(this).attr('href'))

Au retour on ajoute les commentaires dans la page :

$('ol.commentlist').append(data.html)

S’il faut ajouter un bouton pour les commentaires suivants on le fait :

if(data.href !== 'none') {
    $('#nextcomments').attr('href', data.href)
    $('#morebutton').show()
}

Dans le contrôleur Front/CommentController on a ce code :

public function comments(Post $post, $page)
{
    $comments = $this->commentRepository->getNextComments($post, $page);
    $count = $post->parentComments()->count();
    $level = 0;

    return [
        'html' => view('front/comments/comments', compact('post', 'comments', 'level'))->render(),
        'href' => $count <= config('app.numberParentComments') * ++$page ?
            'none'
            : route('posts.comments', [$post->id, $page]),
    ];
}

On voit qu’on retourne une réponse JSON (c’est automatiquement fait par Laravel) avec deux éléments :

  • html : le code HTML des commentaires à ajouter dans la page
  • href : éventuellement l’url pour le bouton des commentaires supplémentaires

En résumé

  • Ajax est facile à mettre en œuvre avec Laravel.
  • On peut faire en sorte que la protection CSRF soit automatiquement mise en œuvre pour toutes les requêtes.

Laisser un commentaire