Guides / Building Search UI / UI & UX patterns

Infinite scroll with Angular InstantSearch

Angular InstantSearch is deprecated. Please use InstantSearch.js instead. For more information, see Migrating from Angular InstantSearch.

An “infinite list” is a common way of displaying results. It’s especially well-suited to mobile devices and has two variants:

  • Infinite hits with a “See more” button at the end of a batch of results. Implement this with InstantSearch’s ais-infinite-hits widget.
  • Infinite scroll uses a listener on the scroll event (called when users have scrolled to the end of the first batch of results). The following guidance implements such an infinite scroll. Find the complete example on GitHub.

If there are no hits, you should display a message to users and clear filters so they can start over.

Display a list of hits

The first step to creating the infinite scroll component is to render the results with the ais-infinite-hits connector. There’s an external Hit component, but it’s not the point of this guide. The intent is to keep the code simple.

Read more about connectors in the customizing widgets guide.

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 you have your results, the next step is to track the scroll position to determine when the rest of the content needs to be loaded (using the Intersection Observer API). Use the API to track when the bottom of the list (the “sentinel” element) enters the viewport. You can reuse the same element across different renders. The Web Fundamentals website discusses the use of this API in more detail.

Use the Intersection Observer API to create a directive that detects when an element is visible inside the viewport.

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
36
37
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() {
    if (this._intersectionObserver) {
      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 declare it in the 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 {}

This implementation uses the Intersection Observer API. To support Internet Explorer 11 you need a polyfill for IntersectionObserver. A browser API is used in the example, but you can apply the concepts to any infinite scroll library.

Retrieve more results

Now that you can track when you reach the end of the results, use the showMore function exposed in the template to the 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>

Show more than 1,000 results

To ensure excellent performance, the default limit for the number of hits you can retrieve for a query is 1,000. When building an infinite scroll, you should stay within this limit.

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

Increasing the limit doesn’t mean you can 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.