How to create an Infinite Scroll natively with Liferay

Légende

1. Liferay : introduction

Liferay is a digital platform allowing you to offer a personalized experience to each of its visitors. Liferay offers CMS, DXP, EDM, Low-Code/No-Code, Publications, Blogs, Experiences... in short, a truly multi-faceted tool.

One of the most interesting features of Liferay has always been the asset publisher which allows you to display content (blogs, articles, documents, objects, etc.) by applying filters, orders and paginations, all dynamically and simply without the need for production.

However, it is possible to only display a certain number of elements or to paginate but not simply to perform an infinite scroll as is usually done on a mobile for example.

It is possible to create an Application Display Template ("ADT") but rendering it is not so simple to do in AJAX with the widget URL.

If you want to do it this way, the following article has long been a reference: https://liferay.dev/fr/blogs/-/blogs/divide-and-conquer-rendering-structured-web-content-with-the-asset-publisher

But since then the fragments have been created, a new way of proceeding has appeared.

Indeed, it is now possible to save manual configurations of the asset publisher and make collections that we will be able to query via JSON APIs which will allow us to call them via remote applications (or customElements) and thus be able to paginate, order, sort on it via the APIs.

It is also possible to create your own collections via code.

 

2. Liferay - The HeadLess

The idea would therefore be to replace the portlet (widget) part of the asset publisher but to retain its strength to create configurable collections natively.

It is also possible to create a new Liferay Object which also exposes all its APIs in REST or GraphQL but unlike web content, we would not be able to benefit from the back of Liferay (editor, display template, etc.) even if side of Liferay objects many functionalities are now possible (display page, location, states, workflow...)

Firstly, you must create a new collection which can be done via the content aggregator, via code (collection provider) or even manually, as we can see here with the creation of a collection which will have as the technical name “ma-collection”:

In this example, a new "Tshirt" structure has been created with an image, a title, a description, a price, a size... and several "Tshirt" articles have been created in order to retrieve a list of articles that we will be able to paginate.


But also 2 display templates, one for card display (Card) and the other for detailed display (Detail) : 


 

So far, nothing very special compared to the "Divide & Conquer" article and this is intentional because we are not going to reinvent everything that has already been designed on our existing site.

It is for the recovery of these articles that we will have to call on an internal Liferay webservice which will be called from our “Scroll Infinite” fragment.

This "endpoint" is available natively via Liferay's internal API ("/o/api") :


 

In Liferay a collection is also called a "content-set".

And to create the url in javascript, we will proceed as follows:

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

We will therefore make a call to this URL with a classic fetch and providing it with the 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; 
...

A list of Web contents will therefore be received as well as the existing total knowing that I only requested a part (a page of X elements).

Once this list has been retrieved, the idea is to ask to display each of the Web contents using the "Card" template and to do this, we will once again make a call to Liferay's internal API :

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

With this "endpoint", we will directly retrieve the HTML of the desired template and add it as is to our 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);
  });

The idea is therefore to have on our page an existing "div" named "cardContainer" and to insert the HTML of the "Card" into it and as I want a configurable fragment, I will add to my "div" the desired Bootstrap class ("col-4" if I want 3 cards per line, "col-12" if I want them one under the other...) hence my variable "divToAddOnCard" that I initialized in my configuration.

 

3. Liferay - The fragments

In this screenshot, we see that I have configured my "Scroll Infinite" fragment to display articles 3 by 3 by putting the "col-4" div there using the key model 40346 from my collection which I called "ma-collection" which I spoke about at the beginning of this article :

So I have everything I technically need to create my “scroll-infinite” fragment.

The idea is therefore to display X "skeletons" and in my case it will be 3 :

And to trigger my first call which will retrieve my first 3 articles and insert them above my 3 skeletons :

Then below an area which will display the progress and the number of articles to load. This area, once displayed on the screen, will trigger another call for 3 articles and so on until reaching the end of the display of all my articles and in this case the deletion of my 3 skeletons.

Here is the complete code of the HTML part of my 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>

Ultimately it's quite simple, a "card-container" div, a "loader" part which will contain my X skeletons and which I will delete at the end if I no longer need them and finally my "cards-footer" div which is there to inform of the progress but above all to trigger a new webservice call as soon as it is displayed. (to tell the truth I had to add a little wait of 200ms because Liferay answered me too quickly and we never saw my beautiful skeleton plus I told myself that it also helps relieve Liferay by limiting the calls a little because each page display makes 4 calls in my case)

We see in this screenshot that when I reached the end of my list (only 7 elements in my collection), there are no longer the 3 "skeletons" and we only see a single final "Card" because before there was had the 6 “Cards” (2 pages of 3). In addition, as all the elements are now visible, I can stop my observation, in fact, no need to call back my web services, I have already received everything!

And finally for our super "scroll-infinite" effect, we just need to create an "IntersectionObserver" and its associated method :

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);

Without forgetting to destroy it at the end:

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

Et voilà !  I now have a configurable and native "infinite scroll" fragment (Liferay + VanillaJS) without creating a single line of JAVA and while maintaining control over my fragment, my display template, my web content, my structure and my collection.

I can even create variations of my collection so that certain visitors do not see the same tshirts based on their previous visits or their age or favorite animal...

All this can of course be done with Liferay "Objects" but for the moment the management of objects is less "user-friendly" for contributors even if version 7.4u120 brings even more new features with the display pages for Objects...

And unlike the asset publisher, the entire page is not reloaded each time the pagination changes and it is much more “mobile friendly” !

Do not hesitate to share this article around you or ask me if you want to access the complete sources of this fragment.

If you want to know more about our expertise, whether on a migration or the implementation of a new digital platform, or simple advice, do not hesitate to contact me !
You can find all my articles and those of my colleagues here (in french) : https://niji.tech/

 

Credit : Photo of Tshirt taken from : https://fr.freepik.com

Sponsor : https://www.niji.fr

Blogs