ben_knight
Kilo Guru

Note - This is how I have implemented an infinite scroll before but it looks pretty good. Feedback is welcome.

I have also added a bit more of a complete example to ServiceNow Share! Download it and take a look!

Begin

The first thing to do when creating an infinite scroll widget is to... create a widget. We will create it so that it has no dependencies on anything else so all the work and previewing can be done exclusively within the widget editor which should help keep it simple.

 

HTML

The HTML template we are going to use will just have a scrollable parent and a repeated element that will show each row.

<!-- This ID will be what we use to hook into scroll events -->
<div id="infinite">
  <!-- Here is our simple loop that will display our content -->
  <div ng-repeat="record in c.records track by record.sys_id">
    {{::record.short_description}}
  </div>
</div>

 

CSS

Again to keep it simple we are just going to make the parent a scrollable element of a set height. The height is set as this will not work unless the children take up more vertical room than the parent has. This is because the scroll event will not fire unless the container has some room to scroll. 

$height: 50vh;

#infinite {
  max-height: $height;
  overflow-y: scroll;
}

 

Client Script

Here is where the magic happens. We need to have the following things:

  1. A way to grab records from the database
  2. A place to store them
  3. A way to identify if we need to fetch more data

 

First lets add some variables that we need for handling the various bits of functionality.

var c = this;

c.records = []; // Place to store the fetched records. Referenced in the HTML
c.loading = false; // Don't load records if we are already loading some

var index = 0; // Current page we are loading
var pageSize = 10; // Number of records to fetch per batch
var scrollBuffer = 100; // Distance (px) from bottom of list before loading content

 

Some of these are good candidates for options schema but for now lets just get the widget up and running. Next up we will setup our fetching method. In is case I have chosen to use the Table API, but a widget Server script or other method is fine too.

function getNewBatch() {
  c.loading = true; // Block the fetch of more until this is complete

  // For this fetch we are using the table api.
  var params = [
    'sysparm_offset=' + (index * pageSize), // Set what page to grab
    'sysparm_limit=' + pageSize,            // Set how many records per page
    'sysparm_query=active=true^ORDERBYorder'// query + Order to retrieve records
  ].join('&');

  // Use any retrieval method you like. In this case I am using the REST API
  $http.get("/api/now/table/kb_knowledge?" + params).then(function (result) {
    c.loading = false; // this fetch is complete to unblock the retrieval again
    index++;
    c.records = c.records.concat(result.data.result); // Add the new records to our list
  });
}

// Initial fetch on load of the widget
getNewBatch();

 

Lastly lets add out scroll checks.

// Look at the parent scroller (Currently our root element of this widget),
// and when it reaches the bottom bring in more content to look at
var parentScrollElement = angular.element('#infinite')[0];
parentScrollElement.addEventListener('scroll', function (a) {
  // Not already loading a batch and the scroll is within the buffer	
  if (!c.loading && IsWithinBuffer(parentScrollElement, scrollbuffer)) {
    getNewBatch();
  }
});

/**
 * Checks if the provided elements scroll distance is within the provided
 * number of pixels
 */
function IsWithinBuffer(element, buffer) {
  return element.scrollHeight - element.scrollTop - buffer <= element.clientHeight
}

 

And that is it! we now have an infinitely scrolling widget. Check it out!

find_real_file.png

As you can see the scrollbar starts large and loads more records once it gets within our buffer. Now that we have a template lets look at some options for how to make this beautiful.

 

Image Library

A common feature seen in the wild is an infinitely scrolling image library. Lets recreate that with our current infinitely scrolling knowledge articles. For the sake of simplicity we are going to cheat a little here and just add a few repeating images, but for a real implementation a good idea would be to reference a records attachments, or add an image/url field to retrieve the image from.

 

Client Script

In your instance add a few images to the db_image table and collect their urls. In this case I have added 5 images named "one.jpg" up to "five.jpg". Then you can add them to your client script like below. I have also added some additional column sizing for stylistic purposes.

// Images to display within our scroller
c.images = [
  'one.jpg',
  'two.jpg',
  'three.jpg',
  'four.jpg',
  'five.jpg'
];

// Bootstrap column sizing so we can make some patterns. In this case our infinite
// scroll will appear with the below grid, repeating as we add more:

// [ image ] [ image ]
// [      image      ]
// [img]  [img]  [img]

c.widths = [
  '6',
  '6',
  '12',
  '4',
  '4',
  '4'
];

 

HTML

Once this is complete we will want to display them in our list. Change the HTML template to the following:

<div id="infinite">
  <!-- loop each row. Sets the width through  bootstrap classes in ng-class -->
  <div class="infinite-element" ng-repeat="record in c.records track by record.sys_id" ng-class="::'col-xs-' +  c.widths[$index%c.widths.length]">
    <!-- new image element, getting the source from our array of image urls -->
    <img ng-src="{{::c.images[$index%c.images.length]}}" class="img-responsive" />
    <div class="description">
      {{::record.short_description}}
    </div>
  </div>
</div>

There are a few changes made here. First I have added an image tag, where the source of the image will be selected from the c.images array in our client script. There is also a bit of logic there to loop through them by the index of the row.

I have also made use of the widths array mentioned before to add some tiling of the knowledge articles.

Next I have wrapped the description in its own div. The reason for this is to do with how I want to display the images, as I would like to overlay the text on my images. Lets have a look at the CSS now.

CSS

The key parts of this are that our description is relative to the parent container now, so it will overlay nicely with the image. I have also added some styles so the text is easier to see.

.infinite-element {
  // Set to relative so the descriptions absolute is relative to the parent container
  position: relative;

  // Add a gap between rows of images
  padding-bottom: 1em;
  
  img {
    width: 100%;
  }
  
  .description {
    // Make the description hover over the image on the top left
    position: absolute;
    top: 0;
    left: 0;
    
    // bootstrap gutter calculation so our text starts where the image starts
    margin-left: $grid-gutter-width / 2;
    margin-right: $grid-gutter-width / 2;

    // Give the text a bit of room to make it easier to read
    padding: 0.25em;
    
    // Darken the background to also help the text be legible
    background-color: rgba(0,0,0,0.25);
    color: white;
  }
}

 

Result

You should now have an infinitely scrolling list of knowledge articles and images.

find_real_file.png

 

Next Steps

That should be enough for this tutorial. Some additional things that should be done for an actual implementation for production environments:

  1. Allow users to click on the images to navigate to the article
  2. Instead of using 5 repeated images they should come from an attachment or image field for each record. Another option is to grab them through an API.
  3. Handle the case where there are no more articles to grab. Currently it will just keep checking if a user scrolls at the bottom of the container.
  4. Put in some error handling. Currently it is assumed that both the image exists and that our batching request will always succeed. If either fail then the widget will break in some way.
Comments
Fabian Kunzke
Kilo Sage
Kilo Sage

Love it. One addition to maybe show how big this is: This can be used for other APIs as well including self build ones. It does indeed allow a scrollable dashboard - one of the more flashy use cases i could think of - (e.g. status of business services/eventmanagement). Further keep in mind, that this also can be used as a backen UI Macro for showing KB entries, reports (again) or referenced records WITHOUT loading them imidately - very useful for workspace development (where currently everything is loaded at the same time).

Very nicely done!

ben_knight
Kilo Guru

Yeah the one I have in production actually uses widgets as the child elements rather than images. They are dynamically loaded when scrolling. That really opens it up to being able to do anything you like with it.

jasonkist
Giga Guru

Hello, this is a great thread. Trying to implement, I am getting 'TypeError: Cannot read properties of undefined (reading 'addEventListener')' on the browser when running the widget. Im stumped on what Im missing? Is there a directive I need to import on the client?

Version history
Last update:
‎08-27-2020 08:03 PM
Updated by: