[AngularJs + Php MYSQL ] Une table Html infinite scroll simple, sur base de donnée

angularjsphp_1_

Introduction


Avec Angularjs 1.6 , on ne télécharge pas tout un modèle de données lorsqu’il est très grand. Par exemple, pour, afficher toute une table de log de 150 MO, évidemment, on ne charge pas toute la table d’un seul coup dans la mémoire vive, ce serait bien trop couteux. Dans d’autres cas, dans le cadre d’une vue spéciale dans une application, on peut envisager de télécharger tout le modèle de données ( Par exemple, 500 utilisateurs)

L’idée de l’infinite Scroll sur base de donnée


L’idée étant d’afficher les 20 premiers enregistrements de la table, puis ensuite, lorsque l’on atteints le bas de la page, charger les 20 suivants, puis ainsi de suite, de cette manière, l’utilisateur a la sensation d’être en temps réel.

Le problème


Le problème vient lorsque l’on commence à utiliser des filtres dans le Back End, en effet, cela complique le système car les enregistrements arrivent alors dans le désordre, et nous verront cela dans une seconde partie.

Petit Rappel de l’architecture MVC de AngularJs 1.6 ( != Angular 5) :


file-page1.jpg

UNE IDEE DE LINFINITE SCROLL EN ACTION :

infitie.jpg

PHASE 1 Le code pour une table auto incrémentale, dont aucun enregistrement n’a jamais été supprimé

LA VUE HTML

On commence à créer la table dans la vue comme ceci :

<table class="table table-hover sttable table-responsive table-bordered ">
   <thead>
      <th>Id</th>
      <th>identifiant principal</th>
      <th>sessionId</th>
      <th>ip</th>
      <th>message</th>
      <th>timestamp</th>
      <th>url</th>
      <th>data</th>
      </tr>
   </thead>
   <tbody>
      <tr ng-repeat="row in logs track by $index">
         <td>{{::row.id}}</td>
         <td>{{::row.identifiantP}}</td>
         <td>{{::row.sessionId}}</td>
         <td>{{::row.ip}}</td>
         <td>{{::row.message}}</td>
         <td>{{::row.timestamp}}</td>
         <td>{{::row.url}}</td>
         <td>{{::row.data}}</td>
      </tr>
   </tbody>
</table>

Notes :

  • Remarquer qu’il faut ajouter ‘track by $index’ au ng-repeat pour que cela fonctionne.
  • Les :: sont utiles pour debinder afin de récupérer de la mémoire, en effet, pas besoin de 2 ways binding dans un simple reporting.

Ensuite , dans mon controleur ANGULARJS 1.6, je tape les instructions suivantes :

var startRowId = 0;
$scope.read_logs = function() {

    $http.post(wsUrl, {
            read_logs: JSON.stringify({
                'startRowId': startRowId
            })
        })
        .then(function(data) {

            if (startRowId == 0) {
                $scope.logs = data.data;
            }

            if (startRowId >= 20) {

                angular.forEach(data.data, function(obj) {
                    $scope.logs.push(obj);
                })

            }

        })

}

$scope.read_logs();

angular.element($window).bind("scroll", function() {
    var windowHeight = "innerHeight" in window ? window.innerHeight : document.documentElement.offsetHeight;
    var body = document.body,
        html = document.documentElement;
    var docHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
    windowBottom = windowHeight + window.pageYOffset;
    if (windowBottom >= docHeight) {
        startRowId += 20;
        $scope.read_logs();
    }
});

Explication :

  • startRowId est la valeur de la première ligne de notre tableau et dans la table SQL simultanément (C’est ‘synchro’).
  • $scope.read_logs() est la fonction qui lit et récupère en AJAX le tableau json habituel, si startRowId est = à 0 alors, il crée tout simplement les données dans la table HTML, par contre, si startRowId >= 20, alors il sait qu’il doit AJOUTER(push) au modèle de données les données qui arrivent, et qui sont forcément les 20 suivantes dans la table logs !.
  • angular.element($window).bind(« scroll », function() est la function qui détecte quand on arrive à la fin de la page, et exécute de nouveau $scope.read_logs() tout en incrémentant startRowId de 20 , de manière à dire au back end : ‘Lit moi les 20 suivants ‘
  • angular.forEach lui ajoute au tableau d’objet JSON $scope.logs les nouveaux objets récupérés.

Le Back End qui traite la requête SQL :

// Le web Service
if(!empty($_POST["read_logs"]) AND $_SESSION['auth']) {

	$json = $_POST["read_logs"];;
	$data = json_decode($json);
	echo json_encode($db->read_logs($data),JSON_NUMERIC_CHECK); // Renvoie le tableau JSON
} 

 //~ La fonction, normalement située dans une Classe PHP, je ne peux pas tout mettre ..
 public function read_logs($data) {
     $sql = " SELECT * FROM Log ";
     if ($data - > startRowId) {
         $sql. = " WHERE id >= ".$data - > startRowId;
     }

     $sql. = " LIMIT 20";

     $responseArray = array();
     if ($result = $this - > conn2 - > query($sql)) {
         while ($obj = $result - > fetch_object()) {
             array_push($responseArray, $obj);
         }
         mysqli_free_result($result); // Free result set
     }
     mysqli_close($this - > conn2);
     return $responseArray;
 }

Explication : Si le back end reçois la variable startRowId, alors il crée la requête SQL en conséquence, à chaque fois donc que l’on atteint la fin de la page…

Conclusion :

Voilà , on a un infinite scroll qui fonctionne seulement sur une table auto incrémentale et ou aucun enregistrement n’aura été supprimé.

Cependant, comment faire si l’on veut ajouter des filtres simultanés dans la requête SQL Back End , ou si on a supprimé des lignes (row) dans notre table SQL, du coup l’id auto increment ne respecte plus le n+1 ? Cela complique bien, puisque les ‘rows’ lignes n’arrivent pas forcément dans l’ordre…
C’est ce que nous allons analyser et voir dans la seconde partie.

PHASE 2 Le code pour une table auto incrémentale, dont des enregistrements ont été supprimés

ON voit que si l’on supprime 3 enregistrements au début de notre table SQL, par exemple eux qui ont respectivement les ids 3,4 et 5 , notre script de la phase 1 ne fonctionne plus correctement, et veut toujout nous afficher le slignes à partir de l’id 20 , puis de l’id 40 puis de l’id 60 etc …. Pourquoi ? et bien c’est à cause du ’20’ statique qui est ajouté à chaque fois dans le WHERE de la requête SQL et qui fausse alors notre résultat, si les 20 premiers résultats ne sont pas une suite en n+1 !

La solution est donc de passer l’id du dernier enregistrement acquis par le front endà notre requête SQL en lieu et place du chiffre statique (20 ou 40 ou 60 etc ..), afin qu’elle ne plante jamais ! Autrement dit, à chaque fois qu’une nouvelle page s’affiche, je récupère l’ID de la dernière ligne affichée, puis l’envoie au back end, qui lui, reconstitue la requête avec le dernièr ID obtenu (PAr exemple 23), puis renvoie les 20 prochaines données !

On modifie le code en conséquence :

Le controleur

var startRowId = 0;
$scope.read_logs = function() {

    $http.post(wsUrl, {
            read_logs: JSON.stringify({
                'startRowId': startRowId
            })
        })
        .then(function(data) {

            if (startRowId == 0) {
                $scope.logs = data.data;
            }

            if (startRowId != 0) {

                angular.forEach(data.data, function(obj) {
                    $scope.logs.push(obj);
                })

            }

        })

}

$scope.read_logs();

angular.element($window).bind("scroll", function() {
    var windowHeight = "innerHeight" in window ? window.innerHeight : document.documentElement.offsetHeight;
    var body = document.body,
        html = document.documentElement;
    var docHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight);
    windowBottom = windowHeight + window.pageYOffset;
    if (windowBottom >= docHeight) {
       startRowId = $scope.hk_logs[$scope.hk_logs.length - 1].id;
       $scope.read_hk_logs();
    }
});

Explication : On récupère lorsqu’on a scrollé l’id de la dernière ligne affichée dans le tableau, afin de l’envoyer à la requête SQL

Phase 3 : Mais moi je veux utiliser des filtres aussi, quand je fais de l’infinite Scroll !

En fait, maintenant que l’on a résolu le problème de la dernière ligne (Qu’on aurait également pu traiter dans le back end directement), Ajouter des filtres est plutôt facile, il faut ajouter un select dans le front end, contenant les critères de filtres, puis, envoyer le critère au back end qui l’ajoute à la fin de la requête SQL, dans un WHERE :

Ma requête $http.post envoie désormais des filtres (dans le front end):


$http.post(wsUrl, {
read_logs: JSON.stringify({
'startRowId': startRowId,
'start_month_date': $scope.start_month_date,
'start_year_date': $scope.start_year_date,
'kitIdFilter': $scope.kitIdFilter,
'hkmessageFilter':$scope.hkmessageFilter,
'hkIpFilter':$scope.hkIpFilter
})
})

Puis, mon back end crée la requête SQL en conséquence :

 //~ Read LOGS
    public function read_logs($data)

    {
        $sql  = " SELECT * FROM Log WHERE 1=1";

		if($data->startRowId){

			$sql .= " AND id >= ".$data->startRowId ;
		} 

		if ($data->start_month_date) {

            $sql .= " AND MONTH(timestamp) = '" . $data->start_month_date . "'";
        }
        if ($data->start_year_date)  {

            $sql .= " AND YEAR(timestamp) = '" . $data->start_year_date . "'";
        }

		 if ($data->homekitIdFilter)  {

            $sql .= " AND kitId = '" . $data-><span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>kitIdFilter . "'";
        }

         if ($data->hkmessageFilter)  {

            $sql .= " AND message = '" . $data->hkmessageFilter . "'";
        }

        if ($data->hkIpFilter)  {

            $sql .= " AND ip = '" . $data->hkIpFilter . "'";
        }

		$sql.= " LIMIT 20";

        $responseArray = array();
        if ($result = $this->conn2->query($sql)) {
            while ($obj = $result->fetch_object()) {
                array_push($responseArray, $obj);
            }
            mysqli_free_result($result); // Free result set
        }
         mysqli_close($this->conn2);
        return $responseArray;
    }
Publicités