Laravel 5

Laravel et AngularJS : gestion des rêves

Dans ce dernier article on va s’intéresser à la gestion des rêves. Comment s’effectue la pagination à partir des données fournies par Laravel. Comment on ajoute un rêve, comment on peut aussi en modifier ou en supprimer un.

Nota : cet article est le dernier de la série, le premier se trouve ici.

La pagination

Laravel sait gérer élégamment la pagination en mode étendu (avec des index pour les pages) ou simplifié (avec juste un bouton next et/ou previous). On ne va évidemment pas se priver de cette pagination automatique parce qu’on utilise AngularJS ! Il faut juste trouver le moyen de transmettre et utiliser les informations nécessaires.

On a vu que côté Laravel on procède de façon classique au niveau du repository pour la requête dans la base :

/**
 * Get dreams with user paginate.
 *
 * @param  integer $n
 * @return collection
 */
public function getDreamsWithUserPaginate($n)
{
    $dreams = Dream::with('user')
            ->latest()
            ->simplePaginate($n);

    return $dreams;
}

La réponse est constituée dans le contrôleur en JSON :

return response()->json($this->dreamRepository->getDreamsWithUserPaginate(4));

Voici le début de ces informations pour la page 1 :

{"per_page":4,"current_page":1,"next_page_url":"http:\/\/localhost\/dreams\/dream\/
?page=2","prev_page_url":null,"from":1,"to":4,"data":[{"id":1,"content":"1 Lorem ipsum dolor

On trouve les information pertinentes pour la pagination :

  • per_page : nombre de rêves par page
  • current_page : numéro de la page courante
  • next_page_url : url de la page suivante (null si aucune)
  • prev_page_url : url de la page précédente (null si aucune)

On a ensuite data qui contient tous les rêves et on a vu comment on les affiche lors du précédent article.

Voyons le contrôleur d’AngularJS :

/* Pagination */
$scope.paginate = function (direction) {
    if (direction === 'previous')
        --$scope.page;
    else if (direction === 'next')
        ++$scope.page;
    Dream.get({page: $scope.page},
    function success(response) {
        $scope.data = response.data;
        $scope.previous = response.prev_page_url;
        $scope.next = response.next_page_url;
    },
            function error(errorResponse) {
                console.log("Error:" + JSON.stringify(errorResponse));
            }
    );
};

La méthode paginate permet de demander au serveur la page dont le numéro figure dans la variable $scope.page. Le paramètre direction optionnel permet de modifier la valeur de cette variable si on veut la page précédente ou la suivante. Elle fait appel au service que nous avons déjà vu et au retour les données sont stockées dans les variables appropriées :

  • data : pour les données des rêves
  • previous : pour l’url de la page précédente
  • next : pour l’url de la page suivante

En fait on a pas vraiment besoin des url de la pagination mais on a vu qu’on obtient un null s’il n’y a pas de valeur d’url.

L’affichage de la pagination devient alors facile :

<nav>
    <ul class="pager">
        <li ng-show="previous" class="previous "><a ng-click="paginate('previous')" class="page-scroll" href="#dreams"><< Previous</a></li>
        <li ng-show="next" class="next"><a ng-click="paginate('next')" class="page-scroll" href="#dreams">Next >></a></li>
    </ul>
</nav>

La directive ng-show permet l’affichage ou non selon qu’on a une url ou null dans la variable. La directive ng-click appelle la méthode paginate du contrôleur avec comme paramètre next ou previous selon la direction désirée, ce qui a pour effet d’aller chercher sur le serveur la page concernée et de l’afficher avec la nouvelle pagination :

img98

Créer un rêve

Le but premier du site et de permettre d’ajouter des rêves. Voyons comment ça fonctionne. Lorsqu’un utilisateur est connecté on a vu qu’il dispose d’un formulaire :

img85

Dans la déclaration du formulaire on trouve ce code :

<form ng-controller="DreamCtrl" ng-submit="submitCreate()" accept-charset="UTF-8" role="form">

Cette partie est donc gérée par le contrôleur DreamCtrl de AngularJS. D’autre part la soumission se fait avec la méthode submitCreate de ce contrôleur :

dreamsControllers.controller('DreamCtrl', ['$scope', 'Dream',
    function DreamCtrl($scope, Dream) {

        /* Create Dream */
        $scope.submitCreate = function () {
            $scope.errorCreateContent = null;
            Dream.save({}, $scope.formData,
                function success(response) {
                    $scope.formData.content = null;
                    $scope.$parent.page = 1;
                    $scope.$parent.data = response.data;
                    $scope.$parent.previous = response.prev_page_url;
                    $scope.$parent.next = response.next_page_url;
                    window.location = '#dreams';
                },
                function error(errorResponse) {
                    $scope.errorCreateContent = errorResponse.data.content[0];
                }
            );
        };

    }]);

On voit l’injection du service Dream :

dreamsServices.factory('Dream', ['$resource',
    function ($resource) {
        return $resource("dream/:id", {page: '@page'}, {
            get: {method: 'GET'},
            save: {method: 'POST'},
            delete: {method: 'DELETE'},
            update: {method: 'PUT'}
        });
    }]);

Ce service comporte tout ce qu’il nous faut pour la gestion des rêves. La méthode get est utilisée par la pagination que nous avons vue ci-dessus. Pour la création d’un rêve c’est évidemment la méthode save qui est utilisée.

Côté Laravel on arrive sur la méthode store du contrôleur DreamController :

/**
 * Store a newly created resource in storage.
 *
 * @param  App\Http\Requests\DreamRequest $request
 * @return Response
 */
public function store(DreamRequest $request)
{
    $this->dreamRepository->store($request->all(), auth()->id());

    return response()->json($this->dreamRepository->getDreamsWithUserPaginate(4));
}

On appelle dans un premier temps la méthode store du repository :

/**
 * Store a dream.
 *
 * @param  array  $inputs
 * @param  integer $user_id
 * @return boolean
 */
public function store($inputs, $user_id)
{
    $dream = new Dream;
    $dream->content = $inputs['content'];
    $dream->user_id = $user_id;
    $dream->save();
}

Le rêve est ainsi enregistré dans la table des rêves. Puis le contrôleur envoie la réponse JSON :

return response()->json($this->dreamRepository->getDreamsWithUserPaginate(4));

On voit qu’on fait à nouveau appel au repository pour récupérer la première page des rêves, pourquoi ? Comme il y a ajout d’un rêve celui-ci va être le premier de la liste parce qu’ils sont classés par ordre de date récente. Il est donc judicieux de rafraichir la liste pour que le nouveau rêve apparaisse en tête pour l’utilisateur.

Côté AngularJS il faut alors mettre à jour les informations des rêves :

$scope.$parent.page = 1;
$scope.$parent.data = response.data;
$scope.$parent.previous = response.prev_page_url;
$scope.$parent.next = response.next_page_url;

On efface aussi les information du formulaire :

$scope.formData.content = null;

On fait aussi glisser la page sur les rêves :

window.location = '#dreams';

Modifier un rêve

On doit aussi pouvoir modifier un rêve. Plutôt que d’afficher le formulaire directement sur la page j’ai utiliser une page modale pour changer un peu. Le code de cette page est déjà présent :

<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
                <h4 class="modal-title" id="myModalLabel">Change your dream...</h4>
            </div>
            <div class="modal-body">

                <form ng-submit="submitChange()" accept-charset="UTF-8" role="form">
                    <input type="hidden" name="_token" value="<?php csrf_token() ?>">
                    <div class="row">

                        <div class="form-group col-lg-12" ng-class="{'has-error': errorContent}">
                            <textarea rows="8" ng-model="content" class="form-control" name="content" id="content" required></textarea>
                            <small class="help-block">{{ errorContent}}</small>
                        </div>

                        <div class="form-group col-lg-12 text-center">                        
                            <button type="button" class="btn btn-default"type="submit"  data-dismiss="modal">Close</button>
                            <input class="btn btn-default" type="submit" value="Save changes">
                        </div> 

                    </div>
                </form>                         

            </div>
        </div>
    </div>
</div>

Il suffit donc de l’appeler. L’utilisateur dispose au niveau de ses rêves d’un bouton sous la représentation stylisée d’un stylo :

img01

Avec ce code :

<a ng-click="edit(dream.id, $index)" href>
    <span class="fa fa-fw fa-pencil"></span>
</a>

Un clic appelle la méthode edit du contrôleur de AngularJS :

/* Edit Dream */
$scope.edit = function (id, index) {
    $scope.errorContent = null;
    $scope.id = $scope.data[index].id;
    $scope.content = $scope.data[index].content;
    $scope.index = index;
    $('#myModal').modal();
};

On initialise quelques variables et on affiche la page. Voyons de plus près les variables :

  • errorContent : pour les erreurs de validation, on purge dans le cas où le formulaire a déjà été utilisé
  • id : on a besoin de l’identifiant du rêve à modifier
  • content : on a aussi besoin du contenu du rêve à modifier
  • index : on mémorise l’index du rêve dans la page

La page modale s’affiche :

img02

Voyons la déclaration du formulaire :

<form ng-submit="submitChange()" accept-charset="UTF-8" role="form">

La soumission est confiée à la méthode submitChange du contrôleur de AngularJS :

/* Update Dream */
$scope.submitChange = function () {
    $scope.errorContent = null;
    Dream.update({id: $scope.id}, {content: $scope.content},
    function success(response) {
        $scope.data[$scope.index].content = $scope.content;
        $('#myModal').modal('hide');
    },
        function error(errorResponse) {
            $scope.errorContent = errorResponse.data.content[0];
        }
    );
};

On trouve l’appel au service Dream avec la méthode update. Ce qui aboutit à la méthode update du contrôleur DreamController de Laravel :

/**
 * Update the specified resource in storage.
 *
 * @param  App\Http\Requests\DreamRequest $request
 * @param  int  $id
 * @return Response
 */
public function update(DreamRequest $request, $id)
{
    if ($this->dreamRepository->update($request->all(), $id)) 
    {
        return response()->json(['result' => 'success']);
    }
}

La validation est assurée par la requête de formulaire DreamRequest avec de simples règles :

public function rules()
{
    return [
        'content' => 'required|max:2000',
    ];
}

En cas d’erreur on renvoie un JSON, par exemple :

{"content":["The content may not be greater than 2000 characters."]}

Ce qui est géré ainsi côté AngularJS :

function error(errorResponse) {
    $scope.errorContent = errorResponse.data.content[0];
}

L’erreur est passée dans la variable errorContent. On la retrouve sur le formulaire :

<small class="help-block">{{ errorContent}}</small>

Avec cet aspect :

img03

Si la validation est correcte côté Laravel on met à jour dans la base avec la méthide update du repository :

/**
 * Update a dream.
 *
 * @param  array  $inputs
 * @param  integer $id
 * @return boolean
 */
public function update($inputs, $id)
{
    $dream = $this->getById($id);

    if ($this->checkUser($dream))
    {
        $dream->content = $inputs['content'];
        return $dream->save();
    }
    return false;
}

Remarquez qu’on prend la précaution de vérifier que c’est bien le propriétaire du rêve qui est en train de vouloir le mettre à jour  (ou un administrateur) avec la fonction privée checkUser :

/**
 * Check valid user.
 *
 * @param  App\Dream $dream
 * @return boolean
 */
private function checkUser(Dream $dream)
{
    return $dream->user_id == auth()->id() || auth()->user()->admin;
}

Nota : classiquement cette vérification s’effectue dans la méthode authorize de la requête de formulaire, ce qui a pour effet de générer une réponse d’erreur. Là je me suis contenté de ne pas générer de réponse.

Côté AngularJS c’est ce code qui est activé :

function success(response) {
    $scope.data[$scope.index].content = $scope.content;
    $('#myModal').modal('hide');
},

On actualise le contenu du rêve sur la page et on cache la page modale.

Supprimer un rêve

Pour supprimer un rêve c’est plus simple puisqu’on a pas besoin de formulaire. On dispose d’un bouton stylisé en poubelle :

img01

Avec ce code :

<a ng-click="destroy(dream.id)" href="#dreams">
    <span class="fa fa-fw fa-trash"></span>
</a>

Un clic appelle la méthode destroy du contrôleur de AngularJS :

/* Destroy Dream  */
$scope.destroy = function (id) {
    if (confirm("Really delete this dream ?"))
    {
        Dream.delete({id: id},
        function success() {
            $scope.paginate();
        },
            function error(errorResponse) {
                console.log("Error:" + JSON.stringify(errorResponse));
            }
        );
    }
};

On fait appel cette fois à la méthode delete du service Dream. Ce qui aboutit finalement à la méthode destroy du contrôleur DreamController de Laravel :

/**
 * Remove the specified resource from storage.
 *
 * @param  int  $id
 * @return Response
 */
public function destroy($id)
{
    if ($this->dreamRepository->destroy($id)) {
        return response()->json(['result' => 'success']);
    }
}

On fait appel à la méthode destroy du repository :

/**
 * Destroy a dream.
 *
 * @param  integer $id
 * @return boolean
 */
public function destroy($id)
{
    $dream = $this->getById($id);

    if ($this->checkUser($dream))
    {
        return $dream->delete();
    }
    return false;
}

On effectue la même vérification qu’on a déjà vu pour la modification d’un rêve pour être sûr des droits de l’utilisateur.

Au retour ce code est activé dans le contrôleur de AngularJS :

function success() {
    $scope.paginate();
},

Comme le rêve n’existe plus on se contente de rafraichir les rêves de la page actuelle. Cette façon de procéder recèle un petit bug potentiel si le rêve supprimé est le dernier et le seul sur la page. Étant donné la faible occurrence de cette hypothèse on peut l’ignorer sans trop de souci.

Nous voici arrivés au terme de cette série sur les relations en Laravel et AngularJS, j’espère que le voyage vous a plu.

Print Friendly, PDF & Email

Laisser un commentaire