Comment mettre en place un Scroll Infini avec Liferay

Comment mettre en place un Scroll Infini avec Liferay

1. Liferay : Les origines

Liferay est une plateforme digitale permettant de proposer une expérience personnalisée à chacun de ses visiteurs. Il propose des fonctionnalités de CMS, DXP, GED, Low-Code/No-Code, Publications, Blogs, Expériences... bref un véritable outils multi-facettes.

Depuis toujours, l'une des fonctionnalités les plus intéressantes de Liferay est l'agrégateur de contenus (asset publisher) qui permet d'afficher des contenus (blogs, articles, documents, objets...) en appliquant des filtres, ordres et paginations complexes le tout dynamiquement et simplement, sans besoin de mises en production.

Avec ce composant, il est possible de n'afficher qu'un certain nombre d'éléments, ou bien de paginer, mais pas simplement d'effectuer un scroll infini, comme il est d'usage de le faire sur un mobile par exemple.

Il est possible de créer un modèle d'affichage d'application (Application Display Template ou "ADT") mais le rendu n'est pas si simple à faire en AJAX avec l'url du widget.

Si vous voulez le faire de cette façon, l'article suivant a longtemps été une référence : https://liferay.dev/fr/blogs/-/blogs/divide-and-conquer-rendering-structured-web-content-with-the-asset-publisher

Une nouvelle façon de procéder a fait son apparition : les fragments.

Désormais, il est possible de sauvegarder des configurations manuelles de l'agrégateur de contenus et d'en faire des collections que l'on va pouvoir interroger via des APIs JSON. Celles-ci seront appelables via des applications distantes (remoteApps ou customElements) qui pourront ainsi paginer, ordonner et/ou trier.

Il est aussi possible de créer ses propres collections via du code.


2. Liferay - Le HeadLess

L'idée serait donc de remplacer la partie portlet (widget) de l'agrégateur de contenus mais de conserver sa force pour créer des collections configurables nativement.

Il est aussi possible de créer un nouvel Objet Liferay qui lui aussi expose toutes ses APIs en REST ou GraphQL mais contrairement aux contenus web, nous ne pourrions pas profiter du back de Liferay (éditeur, modèle d'affichage...) même si du côté des objets Liferay beaucoup de fonctionnalités sont désormais possibles (page d'affichage, localisation, états, flux de travail...)

Dans un premier temps, il faut créer une nouvelle collection ce qui peut être fait via l'agrégateur de contenus, via du code (collection provider) ou bien encore manuellement, comme on peut le voir ici avec la création d'une collection qui aura comme nom technique "ma-collection" :

Dans cet exemple, il a été créé une nouvelle structure "Tshirt" avec une image, un titre, une description, un prix, une taille... et ont été créés plusieurs articles "Tshirt", afin de récupérer une liste d'articles que l'on va pouvoir paginer.

Mais aussi 2 modèles d'affichage, un pour l'affichage sous forme de vignette (Card) et l'autre pour l'affichage détaillé (Detail) :

Jusque là, rien de bien spécial par rapport à l'article "Divide & Conquer" (article cité plus haut) et c'est voulu car on ne va pas réinventer tout ce qui a déjà été conçu sur notre site existant.

C'est pour la récupération de ces articles que l'on va devoir faire appel à un webservice interne de Liferay qui sera appelé depuis notre fragment "Scroll Infinite".

Ce "endpoint" est disponible nativement via l'api interne de Liferay ("/o/api") :

Dans Liferay, une collection est aussi appelée "content-set".

Et pour créer l'url en javascript, on procédera de la façon suivante :

        let url = "/o/headless-delivery/v1.0/sites/"
          + Liferay.ThemeDisplay.getScopeGroupId()
          + "/content-sets/by-key/" + configuration.collectionName 
                  + "/content-set-elements?page=" + currentPage 
          + "&pageSize=" + cardIncrease;

On va donc faire un appel à cette URL avec un fetch classique en lui fournissant le authToken :

fetch(url,
    {
        method: "GET",
        cache: "no-cache",
        credentials: "same-origin",
        headers: {
            "Content-Type": "application/json",
            "x-csrf-token": Liferay.authToken
        }
    }).then((rsp) => rsp.json())
    .then(async (obj) => {
        cardLimit = obj.totalCount;
...

Une liste de "web content" sera donc reçue, ainsi que le total existant.

Une fois cette liste récupérée, l'idée est de demander à afficher chacun des "web content" en utilisant le modèle "Card" et pour faire ceci, on va une fois encore faire un appel à l'api interne de Liferay :

let detailUrl = "/o/headless-delivery/v1.0/structured-contents/"
          + obj.content.id 
          + "/rendered-content/" + configuration.templateKey;

Avec ce "endpoint", on va directement récupérer le HTML du modèle souhaité et l'ajouter tel quel à notre page.

fetch(detailUrl,
    {
        method: "GET",
        cache: "no-cache",
        credentials: "same-origin",
        headers: {
            "Content-Type": "application/json",
            "x-csrf-token": Liferay.authToken
        }
    }).then(async (rsp) => await rsp.text())
    .then(async (obj) => {
           console.log(obj);
           const card = document.createElement("div");
           card.className = divToAddOnCard;
           const cardInner = document.createElement("div");
           cardInner.className = "card";
           cardInner.innerHTML = obj;
           card.appendChild(cardInner);
           cardContainer.appendChild(card);
    });

L'idée est donc d'avoir sur notre page une "div" existante nommée "cardContainer" et de lui insérer le HTML de la "Card" et comme nous voulons un fragment configurable, nous allons au passage ajouter à notre "div" la classe Bootstrap souhaitée ("col-4" si nous voulons 3 cartes par ligne, "col-12" nous voulons les avoir l'une sous l'autre...) d'où la variable "divToAddOnCard" initialisée grâce à la configuration du fragment.


3. Liferay - Les fragments

Dans cette capture d'écran, on voit que le fragment "Scroll Infinite" a été configuré pour afficher les articles 3 par 3 en y mettant la div "col-4" en utilisant le modèle de clé 40346 de la collection nommée "ma-collection" dont il était question au début de cet article :

Il y a donc tout ce qu'il faut techniquement pour créer le fragment "scroll-infinite".

L'idée est donc d'afficher X "squeletons" et dans notre cas ça sera 3 :

Et de déclencher le premier appel qui va récupérer les 3 premiers articles et les insérer au dessus des 3 skeletons.

Puis en dessous une zone qui affichera la progression et le nombre d'articles à charger. Cette zone une fois affichée à l'écran, déclenchera un autre appel de 3 articles et ainsi de suite jusqu'à arriver à la fin de l'affichage de tous les articles et dans ce cas la suppression des 3 skeletons.

Voici le code complet de la partie HTML du fragment :

<div id="card-container">
</div>
<div id="loader">
    [#list 0..configuration.numberOfCards-1 as i]
    <div class="${configuration.divToAddOnCard}">
       <div class="skeleton-card"></div>
        </div>    
  [/#list]
</div>
<div class="cards-footer">
  <span>Showing 
    <span id="card-count">0</span> of 
    <span id="card-total">0</span> cards      
  </span>
</div>

Finalement, c'est assez simple, une div "card-container", une partie "loader" qui va contenir les X skeletons, qui seront supprimés à la fin si nous n'en avons plus besoin. Et enfin la div "cards-footer" est là pour informer de la progression, mais surtout pour déclencher un nouvel appel de webservice dès son affichage. (pour information l'ajout d'une petite attente de 200ms peut être utile, car Liferay peut répondre trop vite et on ne verrai jamais le beau skeleton. En plus, cela permet aussi de soulager le Liferay en limitant un peu les appels car chaque affichage de page fait 4 appels dans ce cas)

On voit dans cette capture d'écran qu'arrivé à la fin de la liste (seulement 7 éléments dans la collection), il n'y a plus les 3 "squeletons" et on voit seulement une seule "Card" finale car avant il y a eu les 6 "Cards" (2 pages de 3).

De plus, comme tous les eléments sont désormais visibles, nous pouvons arrêter l'observation. En effet, plus besoin de rappeler les web services nous avons déjà tout reçu !

Et enfin pour notre super effet "scroll-infinite", il suffit de créer un "IntersectionObserver" et sa méthode associée :

    const onIntersection = (entries) => {
        for (const entry of entries) {
            if (entry.isIntersecting) {

                setTimeout(() => {
                    addCards(currentPage + 1);
                }, 200);

                if (currentPage === pageCount) {
                    removeInfiniteScroll();
                }
            }
        }
    };

    const observer = new IntersectionObserver(onIntersection);

Sans oublier de le détruire à la fin :

    const removeInfiniteScroll = () => {
        loader.remove();
        observer.unobserve(document.querySelector('#card-total'));
    };

Et voilà ! Nous avons désormais un fragment "scroll infinite" configurable et natif (Liferay + VanillaJS) sans création d'une seule ligne de JAVA et tout en gardant la maitrise sur le fragment, les modèles d'affichage, les contenus web, la structure et la collection.

Il est possible de créer des variations (ou variantes) de la collection pour que certains visiteurs ne voient pas les mêmes tshirts en fonction de leurs précédentes visites, ou de leur âge, ou leur animal préféré...

Tout ceci peut bien sûr être fait avec des "Objects" Liferay, mais pour le moment la gestion des objets est moins "user-friendly" pour les contributeurs, même si la version 7.4u120, apporte encore plus de nouveautés, avec les pages d'affichage pour les Objets...

Et contrairement à l'agrégateur de contenus (asset publisher), toute la page n'est pas rechargée à chaque changement de la pagination et c'est beaucoup plus "mobile friendly" !

Petite vidéo du rendu final du "scroll-infinite"

N'hésitez pas à partager cet article autour de vous ou à me solliciter si vous voulez accéder aux sources complètes de ce fragment.

Si vous voulez en savoir plus sur notre expertise, aussi bien sur une migration ou la mise en place d'une nouvelle plateforme digitale, ou un simple conseil, n'hésitez pas à me contacter !

Crédits : Photos de Tshirt extrait de : https://fr.freepik.com/

Sponsor : https://www.niji.fr