05 Oct 2018

Infinite Scroll

Introduction

For very visual websites, such as those built around photo sharing, infinite scroll can be a nice experience for users, and implementing it with Algolia is straightforward.

In this tutorial we’ll see how we can:

  • go further than 1000 hits
  • to an infinite scroll

Dataset

[
  {
    "name": "Catherine Missal",
    "rating": 4875,
    "image_path": "/g3fsRgEoMxaqPayIMtGDWERqJ6A.jpg",
    "alternative_name": null,
    "objectID": "551486300"
  },
  [...]
]

In this tutorial we make use of the name and image_path only.

You can download the dataset here. Have look at how to import it in Algolia here

Initialization

<?php
// composer autoload
require __DIR__ . '/vendor/autoload.php';

// if you are not using composer
// require_once 'path/to/algoliasearch.php';

$client = new \AlgoliaSearch\Client('YourApplicationID', 'YourAdminAPIKey');

$index = $client->initIndex('your_index_name');
require 'rubygems'
require 'algoliasearch'

Algolia.init(application_id: 'YourApplicationID',
             api_key:        'YourAPIKey')
index = Algolia::Index.new('your_index_name')
// var algoliasearch = require('algoliasearch');
// var algoliasearch = require('algoliasearch/reactnative');
// var algoliasearch = require('algoliasearch/lite');
// import algoliasearch from 'algoliasearch';
//
// or just use algoliasearch if you are using a <script> tag
// if you are using AMD module loader, algoliasearch will not be defined in window,
// but in the AMD modules of the page

var client = algoliasearch('YourApplicationID', 'YourAPIKey');
var index = client.initIndex('your_index_name');
from algoliasearch import algoliasearch

client = algoliasearch.Client("YourApplicationID", 'YourAPIKey')
index = client.init_index('your_index_name')
let client = Client(appID: "YourApplicationID", apiKey: "YourAPIKey")
let index = client.index(withName: "your_index_name")
Client client = new Client("YourApplicationID", "YourAPIKey");
Index index = client.getIndex("your_index_name");
AlgoliaClient client = new AlgoliaClient("YourApplicationID", "YourAPIKey");
Index index = client.InitIndex("your_index_name");
 // Synchronous version
 APIClient client = new ApacheAPIClientBuilder("YourApplicationID", "YourAPIKey").build();

 // Asynchronous version
 AsyncAPIClient client = new AsyncHttpAPIClientBuilder("YourApplicationID", "YourAPIKey").build();

 // Google AppEngine
 APIClient client = new AppEngineAPIClientBuilder("YourApplicationID", "YourAPIKey").build();

 Index index = client.initIndex("index");
import "github.com/algolia/algoliasearch-client-go/algoliasearch"

func main() {
  client := algoliasearch.NewClient("YourApplicationID", "YourAPIKey")
  index := client.InitIndex("your_index_name")
}
// No initIndex
val client = new AlgoliaClient("YourApplicationID", "YourAPIKey")

Configuring the index

We are not going to focus on configuring the relevance of the index as the goal is just to do an infinite scroll.

By default Algolia limit the number of hits you can retrieve for a query to 1000; when doing an infinite scroll, you usually want to go over this limit.

<?php
$index->setSettings([
  'paginationLimitedTo' => 0 // Disable the limit
]);
index.set_settings(
  paginationLimitedTo: 0
)

Disabling the limit does not mean that we will be able to go until the end of the hits, but just that Algolia will go as far as possible in the index to retrieve results in a reasonable time.

HTML

<!DOCTYPE HTML>

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.js@2.10/dist/instantsearch.min.css">
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@2.10/dist/instantsearch.min.js">
<!-- Always use `2.x` versions in production rather than `2` to mitigate any side effects on your website,
Find the latest version on InstantSearch.js website: https://community.algolia.com/instantsearch.js/v2/guides/usage.html -->
</script>

<input type="text" id="search" />


<div id="hits-container"></div>

We recommend using jsDelivr only for prototyping, not for production applications. Whenever possible, you should host your assets yourself or use a premium CDN service. jsDelivr is a free service and isn’t operated by Algolia, so we won’t be able to provide support if it fails.

Javascript

We start by initializing instantsearch and the search input widget:

var search = instantsearch({
  appId: "YourApplicationID",
  apiKey: "YourSearchOnlyApiKey",
  indexName: "indexName"
});

search.addWidget(
  instantsearch.widgets.searchBox({
    container: "#search",
    placeholder: "Search for actors"
  })
);

The next step is to add the infinite scroll widget which is part of InstantSearch.js:

search.addWidget(
  instantsearch.widgets.infiniteHits({
    container: "#hits-container",
    templates: {
      empty: "No results",
      item:
        '<div class="hit"><img src="http://image.tmdb.org/t/p/w300/{{image_path}}" /><div class="actor_name">{{name}}</div></div>'
    },
    escapeHits: true
  })
);

Our dataset is extracted from TMDB and the image_path stored in the index need to be prefixed by http://image.tmdb.org/t/p/w300/

Note that the default widget will use a button at the bottom of the page to load the next page. If we want to make sure the next page loads automatically, we can make a custom widget using connectInfiniteHits. We will make this using an Intersection Observer. First we need to instantiate the render function of this custom infinite hits:

Note that the Intersection Observer API isn’t yet widely supported. You may want to consider using a polyfill. Here the compatibility table to find more information about its support.

function customInfiniteHits(opts) {
  var container = opts.container;
  var escapeHits = opts.escapeHits;
  var transformItems = opts.transformItems;
  var containerNode = document.querySelector(container);

  var infiniteHitContext = {
    didRender: false,
    isLastPage: false
  };
  function render(renderingOptions, isFirstRendering) {
    if (isFirstRendering) {
      infiniteHitContext.observer = new IntersectionObserver(
        (entries, observer) =>
          entries.forEach(entry => {
            if (
              infiniteHitContext.didRender &&
              !infiniteHitContext.isLastPage &&
              entry.isIntersecting
            ) {
              observer.unobserve(entry.target);
              infiniteHitContext.didRender = false;
              renderingOptions.showMore();
            }
          })
      );
    }

    containerNode.innerText = "";
    // render the hits with vanilla JS
    renderingOptions.hits.forEach(function createHit(hit) {
      var el = document.createElement("div");
      el.className = "hit";

      var img = document.createElement("img");
      img.src = "https://image.tmdb.org/t/p/w300/" + hit.image_path;
      // fixed size because otherwise when the image is loading, it can make the sentinel visible
      // this can also be done in CSS
      img.style.height = "80px";
      el.appendChild(img);

      var name = document.createElement("div");
      name.className = "actor_name";
      name.innerHTML = hit.name;
      el.appendChild(name);

      containerNode.appendChild(el);
    });

    // this is an extra element to detect when to ask for new items
    var sentinel = document.createElement("div");
    containerNode.appendChild(sentinel);
    infiniteHitContext.observer.observe(sentinel);
    infiniteHitContext.didRender = true;
    infiniteHitContext.isLastPage = renderingOptions.isLastPage;
  }

  function unmount() {
    containerNode.innerText = "";
    if (infiniteHitContext.observer) {
      infiniteHitContext.observer.disconnect();
    }
  }

  var makeInfiniteHits = instantsearch.connectors.connectInfiniteHits(
    render,
    unmount
  );
  return makeInfiniteHits({ escapeHits, transformItems });
}

Then we can create the widget and use it as before, except that now we implemented the templates in the widget itself.

search.addWidget(
  customInfiniteHits({
    container: "#hits-container",
    escapeHits: true
  })
);

This works as following:

  1. The function customInfiniteHits will return a widget, just like the functions that InstantSearch provides
  2. every time the widget needs to be rendered, the inner render function gets called
  3. This render function will make new elements into your container every time
  4. The first time render gets called, we also add an Intersection Observer
  5. if the user is at the bottom of the page, and there are still pages left, we request a next page to be loaded
  6. we also say that we shouldn’t try to try to find out if it’s at the bottom again, until those results are rendered.
© Algolia - Privacy Policy