Afficher un loader pendant une requête AJAX

Attention

Une nouvelle version de cet article a été écrite, mais je laisse celle-ci pour que vous puissiez juger des améliorations apportées au code.

Afficher un loader pendant une requête AJAX – v2

Cas classique, une requête AJAX

Mise en situation. Vous faites une requête AJAX qui peut prendre plus ou moins de temps, et vous voulez informer l’utilisateur que des données sont en train d’être chargées. Pour cela rien de plus simple, il suffit de :

  • Déclarer un booléen
  • Le mettre à true quand la requête est lancée
  • Le mettre à false quand le requête se termine
  • Afficher un loader animé (aussi appelé spinner) tant que le booléen est à true

Cas plus complexe, plusieurs requêtes AJAX à la fois

Que se passe-t-il si on utilise la technique ci-dessus pour plusieurs requêtes ? La première requête se lance, le booléen passe à true. La deuxième requête se lance, le booléen reste à true. Une des deux requêtes se termine, le booléen passe à false. Donc le loader est masqué alors qu’une requête est toujours en cours.

En fait il existe une technique très simple, qui fonctionne quel que soit le nombre de requêtes que vous lancez. Il suffit d’utiliser un integer au lieu d’un booléen. (Merci à mon patron pour l’astuce ^^)

 

Demo sur plnkr.coTélécharger la source

 

index.html

<!DOCTYPE html>
<html lang="en" ng-app="myApp">

  <head>
    <meta charset="UTF-8" />
    <title>AJAX Spinner</title>
    <link href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
  </head>

  <body ng-controller="MainCtrl">
		<br>
		<p>We run 5 requests to retrieve data from the Dailymotion API</p>
		<p>The spinner is displayed while at least one request is still running</p>

		<div class="wrapper">

			<!-- Run multiple AJAX requests -->
			<!-- While something is loading, the button is disabled -->
			<button type="button" class="btn btn-primary" ng-click="getDataFromDM()" ng-disabled="isSomethingLoading">Run 5 AJAX requests</button>

			<!-- While something is loading, the loading spinner is displayed -->
  		<div class="spinner" ng-show="isSomethingLoading"></div>

			<br><br>
			<p ng-repeat="result in results">Request done for the keyword: <span class="bold">{{result}}</span></p>
		</div>

    <script src="https://code.angularjs.org/1.2.9/angular.min.js"></script>
    <script src="app.js"></script>
  </body>

</html>

app.js

var app = angular.module('myApp', []);

app.controller('MainCtrl', ['$scope', '$http',
	function($scope, $http) {

		/* PUBLIC VARIABLES
		================================================== */

		//While this variable > 0, we display the loading spinner
		$scope.isSomethingLoading = 0;

		//Results displayed in the view
		//For the example, will only contain the searched term
		$scope.results = [];

		/* PRIVATE FUNCTIONS
		================================================== */

		//Generate a random string
		//Credit: http://stackoverflow.com/a/1349426/962893
		function generateString() {
			var text = "";
			var possible = "abcdefghijklmnopqrstuvwxyz";
			for( var i=0; i < 3; i++ )
					text += possible.charAt(Math.floor(Math.random() * possible.length));
			return text;
		}

		//Get a lot of data from the Dailymotion API
		function getDataFromDMAPI(keyword) {
			//Clear the previous results
			$scope.results = [];
			//When a request starts, isSomethingLoading is incremented
			$scope.isSomethingLoading++;
			$http({
				method: 'GET',
				url: 'https://api.dailymotion.com/videos?fields=3d,access_error%2Cchannel%2Cdescription%2Cduration%2Csharing_urls%2Csoundtrack_info%2Csources%2Cstart_time%2Cstatus%2Cstream_h264_hd1080_url%2Cstream_h264_hd_url%2Cstream_h264_hq_url%2Cstream_h264_ld_url%2Cstream_h264_url%2Cstream_source_url%2Cstrongtags%2Csvod%2Cswf_url%2Csync_allowed&limit=100&search=' + keyword
			})
				.success(function(data, status, headers, config) {
					//When a request is done, isSomethingLoading is decremented
					$scope.isSomethingLoading--;
					//The results are displayed (only the keyword for this example)
					$scope.results.push(keyword);
				});
		}

		/* PUBLIC FUNCTIONS
		================================================== */

		//Run multiple AJAX Requests
		//Note that if we didn't use random strings, the demo would only work once because the results are cached by the browser (i.e. you wouldn't have the time to see the spinner)
		$scope.getDataFromDM = function () {
			for (var i = 1; i < 6; i++) {
				getDataFromDMAPI(generateString());
			}
		};

	}
]);

Explication du code

Au lieu d’utiliser un booléen, on utilise un integer.

$scope.isSomethingLoading = 0;

Le loader est masqué tant que la variable est à 0 / est affiché tant que la variable est supérieure à 0.

<div class="spinner" ng-show="isSomethingLoading"></div>

Quand une requête est lancée, la variable est incrémentée.

$scope.isSomethingLoading++;

Quand une requête se termine, la variable est décrémentée. De cette façon, la variable ne redescendra à 0 que lorsque toutes les requêtes seront résolues !

$scope.isSomethingLoading--;

Pour aller plus loin

Voilà, l’astuce est toute bête. Mais voici quelques idées pour améliorer ce code :

  • N’afficher le loader que si la requête prend du temps, en effet si celle-ci est très rapide on ne verrait le loader qu’une fraction de seconde
  • Rajouter un effet de fade sur l’apparition et la disparition du loader (en CSS ou en javascript)
  • Mettre un durée de vie maximale au loader pour éviter qu’il reste affiché éternellement si une requête met trop de temps à être résolue

Share Button

7 thoughts on “Afficher un loader pendant une requête AJAX

  1. Geoiris

    Ceci est intéressant mais je pense que tu peux améliorer la chose en creusant du coté des intercepteurs 🙂

  2. Très bonne approche pour gérer les multiples requêtes en parallèle.

    Effectivement les intercepteurs http vont permettre de totalement découpler le spinner du reste de l’application en s’attachant à n’importe quelle requête et d’avoir un module 100% réutilisable. un très bon exemple ici : http://stackoverflow.com/a/17850865/174027

    Attention dans ton code a également prendre en compte les requêtes qui échouent (404, 500…) auquel cas il faut également décrémenter le compte du nb de requêtes en cours sinon le spinner ne se désactivera jamais.

    Renommer « isSomethingLoading » en « currentRequestsCount » aiderait à la lisibilité 🙂

  3. Jordi Maylin

    Merci à vous deux, je ne connaissais pas les interceptors.

    Donc je vais soit mettre à jour cet article, soit faire un nouvel article sur le sujet.

  4. Deux remarques :
    – Il faut que les auteurs de tutoriaux Angular cessent d’écrire de la logique dans les contrôleurs. C’est souvent repris tel quel par les développeurs et on retrouve ça dans les applications professionnelles. Angular est une archi orientée service, il faut systématiquement écrire la logique dans des services et le faire dans les tutoriaux pour faire comprendre aux dev que c’est la bonne pratique.
    – Je regrette l’utilisation directe du scope pour gérer ce problème. Une meilleure pratique aurait été de créer un service.

    Sinon merci pour cette base de connaissances.

  5. このライトアップのためにアップスポット、私は真 | ウェブサイト要求素晴らしい もっとたくさん考察。私そうによ仕上げまでもう一度読むためにたくさん もっと、情報。このような非常に良い良いのため

  6. 一部純粋驚異|代わって仕事関数 のこのインターネットの所有者に完全 、優れました コンテンツ。魅力的な議論を

  7. 私はこれで感動ウェブサイト、実際 |ファン私は私は。 ウェブログ | D.

Laisser un commentaire

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

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>