Guides / Building Search UI / Resources / UI and UX patterns

Infinite Scroll

You are reading the documentation for Angular InstantSearch v3, which is in beta. You can find the v2 documentation here.

Overview

Infinite scrolling is a very common pattern to display a list of results. Most of the times this pattern comes with two variants:

  • with a button to click on at the end of the list of results
  • with a listener on the scroll event that is called when the list of results reaches the end

We can cover those two different implementations with Angular InstantSearch. The former is covered by the built-in ais-infinite-hits widget and the latter is covered by the infiniteHits connector. In this guide we will focus on the second implementation using the Intersection Observer API. We choose to use a browser API in the example but the concepts can be applied to any kind of infinite scroll library. You can find the complete example on GitHub.

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.

Display a list of hits

The first step to create our infinite scroll component is to render the results with the ais-infinite-hits connector. We have an external Hit component but it’s not the point of this guide, the intent is to keep the code simple. You can find more information about the connectors API in the dedicated guide about widget customisation.

1
2
3
4
5
6
7
8
9
10
11
12
<!-- app.component.html -->
<ais-instantsearch [config]="config">
    <ais-search-box></ais-search-box>
    <ais-infinite-hits>
        <ng-template let-hits="hits" let-results="results" let-refine="showMore" >
            <div *ngFor="let hit of hits">
                <ais-highlight attribute="name" [hit]="hit"></ais-highlight>
            </div>
            <button (click)="refine()">Show More</button>
        </ng-template>
    </ais-infinite-hits>
</ais-instantsearch>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app.component.ts
const searchClient = algoliasearch(
  "latency",
  "6be0576ff61c053d5f9a3225e2a90f76"
);

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  public config = {
    indexName: "instant_search",
    searchClient
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { NgAisModule } from "angular-instantsearch";

import { AppComponent } from "./app.component";

@NgModule({
  declarations: [AppComponent],
  imports: [NgAisModule.forRoot(), BrowserModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Track the scroll position

Once we have our list of results the next step is to track the scroll position to determine when the rest of the content needs to be loaded. For this purpose we are gonna use the Intersection Observer API. To track when the bottom of the list enters inside the viewport we observe a “sentinel” element. We use this trick to avoid observing all the items of our results. We can reuse the same element across the different renders. You can find more information about this pattern on the Web Fundamentals website.

Let’s create a simple directive that detects when an element is visible inside the viewport using Intersection Observer API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { AfterViewInit, Directive, ElementRef, EventEmitter, OnDestroy, Output } from "@angular/core";

@Directive({ selector: "[onVisible]" })
export class OnVisibleDirective implements AfterViewInit, OnDestroy {
  @Output() public onVisible: EventEmitter<any> = new EventEmitter();

  private _intersectionObserver?: IntersectionObserver;

  constructor(private _element: ElementRef) {}

  public ngAfterViewInit() {
    this._intersectionObserver = new IntersectionObserver(entries => {
      this.checkForIntersection(entries);
    }, {});
    this._intersectionObserver.observe(<Element>this._element.nativeElement);
  }

  public ngOnDestroy() {
    this._intersectionObserver.disconnect();
  }

  private checkForIntersection = (
    entries: Array<IntersectionObserverEntry>
  ) => {
    entries.forEach((entry: IntersectionObserverEntry) => {
      const isIntersecting =
        (<any>entry).isIntersecting &&
        entry.target === this._element.nativeElement;

      if (isIntersecting) {
        this.onVisible.emit();
      }
    });
  };
}

Now let’s declare it in our main app module and use it.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { NgAisModule } from "angular-instantsearch";

import { AppComponent } from "./app.component";

@NgModule({
  declarations: [AppComponent],
  imports: [NgAisModule.forRoot(), BrowserModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Retrieve more results

Now that we are able to track when we reach the end of our results we can hook the showMore function exposed in the template to our onVisible directive.

1
2
3
4
5
6
7
8
9
10
11
12
<!-- app.component.html -->
<ais-instantsearch [config]="config">
    <ais-search-box></ais-search-box>
    <ais-infinite-hits>
        <ng-template let-hits="hits" let-results="results" let-refine="showMore" >
            <div *ngFor="let hit of hits">
                <ais-highlight attribute="name" [hit]="hit"></ais-highlight>
            </div>
            <div (onVisible)="refine()"></div>
        </ng-template>
    </ais-infinite-hits>
</ais-instantsearch>

Go further than 1000 hits

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

1
2
3
$index->setSettings([
  'paginationLimitedTo' => 1000
]);

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.

That’s it! Now you should have a complete infinite scroll experience! Don’t forget that the complete source code of the example is available on GitHub.

Did you find this page helpful?