Ionic Virtual Scroll with List Headers
Tags: ionic, angular, webdev, typescript
Published: 2020-10-31 17:57:07

We all have apps where we need to display a long list of contacts or users of some sorts. It could be a list of friends, co-workers, phone contacts, or plenty of other use cases. Nevertheless, [`ion-virtual-scroll`](https://ionicframework.com/docs/api/virtual-scroll) comes in handy to create these lists because it comes with built-in performance benefits. As Ionic says in their documentation, “not every record in the list is rendered at once; instead a small subset of records (enough to fill the viewport) are rendered and reused as the user scrolls.” This makes for a smooth scrolling experience for potentially very long lists with heavy data.

We will start by creating a new blank Ionic Angular project, navigate into that directory, and serve it so we can begin to see the changes while we code:

```
$ ionic start ionic-vs-example blank --type=ionic-angular
$ cd ionic-vs-example
$ ionic serve
```

When opening the newly created project, we will have the default blank project setup. We can then generate a service to handle the logic for pulling in our fake contacts data for this example:

```
$ ionic g service contacts
```

After opening up the new `contacts.service.ts` file we can add a function to request contacts from our API. Our page will ping this API on initialization and subsequently fill the virtual list. In this case, I’m going to use [randomuser.me](https://randomuser.me/) to simulate a potential requests for contacts data. In order to make this web request, we’ll first need to import the `HttpClientModule` inside of `app.module.ts`, as well as importing `HttpClient` into the contacts service itself. Then, we can use the HttpClient to ping our API for some results. This should look something like the following:

```
// app.module.ts
...
import { HttpClientModule } from "@angular/common/http";

@NgModule({
...
  imports: [
    BrowserModule,
    IonicModule.forRoot(),
    AppRoutingModule,
    HttpClientModule,
  ],
...
})
export class AppModule {}
```

```
// contacts.service.ts
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";

@Injectable({
  providedIn: "root",
})
export class ContactsService {
  constructor(private http: HttpClient) {}

  getContacts() {
    return this.http.get<any>("https://randomuser.me/api", {
      params: {
        results: "300",
      },
    });
  }
}
```

In order to properly use the contacts’ properties, you could create an interface for your API’s results to give context for each object. In doing so, it would look like this:

```
// contact.ts
export interface Contact {
  name: ContactName;
  email: string;
}

export interface ContactName {
  title: string;
  first: string;
  last: string;
}
```

Now, inside of our `home.page.ts` file, we can ping the `contacts.service.ts` for a list of contacts and assign the results to our global contacts variable. This variable will hold our array of contacts that will be used to build the virtual list.

```
import { Component, OnInit } from "@angular/core";
import { Contact } from "../contact";
import { ContactsService } from "../contacts.service";

@Component({
  selector: "app-home",
  templateUrl: "home.page.html",
  styleUrls: ["home.page.scss"],
})
export class HomePage implements OnInit {
  contacts: Array<Contact> = new Array<Contact>();

  constructor(private contactsService: ContactsService) {}

  ngOnInit() {
    this.contactsService.getContacts().subscribe((res) => {
      this.contacts = res["results"];
      console.log(this.contacts);
    });
  }
}
```

Since we have an array of contacts, we can now finally setup our `home.page.html` file to show the virtual list. The `ion-virtual-scroll` accepts these contacts and allows us to create virtual items for each. We’ll loop over the contacts in the html and show the contact’s full name and email address:

```
//home.page.html
<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title> Contacts </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-virtual-scroll [items]="contacts">
    <ion-item *virtualItem="let item">
      <ion-label>
        <h2>{{item.name.first}} {{item.name.last}}</h2>
        <h4>{{item.email}}</h4>
      </ion-label>
    </ion-item>
  </ion-virtual-scroll>
</ion-content>
```

The resulting virtual list will scroll seamlessly, loading only the content that is visible within the DOM:

<a href="https://www.loom.com/share/54b19378fa364eeb8e52848422c645a4"> <p>Ionic Virtual Scroll - Watch Video</p> <img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/54b19378fa364eeb8e52848422c645a4-with-play.gif"> </a>

## List Headers

Once we have a smooth scrolling list, it’s common that we’d want to group the contacts by some sort of alphabetical order. In this case, I want to sort the contacts by their last name. So first, we’ll want to modify the sorting order to put the contacts in order by their last name. That will result in something like the following:

![](https://connersimmons.dev/wp-content/uploads/2020/10/Screen-Shot-2020-10-25-at-7.31.33-PM-1024x651.png)

The next step will be to break up the contacts using headers. Headers will be used to break up the contacts by the first initial of their last name. This will create a definitive grouping that will make it easier to identify the contact you’re looking for. Ionic’s ion-virtual-scroll allows you to inject a header function on the list. This header function gives you access to the contacts for comparison so you can determine where to put the header. We’ll create the header function and look for when the first initial of the last name of one contact does not match the next. That’s where we’ll place the header. The code will look as follows:

```
// home.page.html
...

<ion-content [fullscreen]="true">
  <ion-virtual-scroll [items]="contacts" [headerFn]="myHeaderFn">
    <ion-item-divider *virtualHeader="let header">
      {{ header }}
    </ion-item-divider>
    <ion-item *virtualItem="let item">
      <ion-label>
        <h2>{{item.name.first}} {{item.name.last}}</h2>
        <h4>{{item.email}}</h4>
      </ion-label>
    </ion-item>
  </ion-virtual-scroll>
</ion-content>
```

```
// home.page.ts
...
export class HomePage implements OnInit {
  ...

  myHeaderFn = (record, recordIndex, records) => {
    let result = null;
    if (recordIndex !== 0) {
      const c1 = record;
      const c2 = records[recordIndex - 1];
      if (c1.name.last != null && c2.name.last != null) {
        if (
          c1.name.last.charAt(0).toUpperCase() !==
          c2.name.last.charAt(0).toUpperCase()
        ) {
          result = c1.name.last.charAt(0).toUpperCase();
        }
      }
    } else {
      const name = record.name.last;
      if (name != null) {
        result = name.charAt(0).toUpperCase();
      }
    }
    return result;
  };
}
```

The `myHeaderFn` looks at the current record and the previous record, so long as we’re not at the first item in the list. At the first index, we simply return the first character of the last name. For the remaining indexes, we look to see if the first letter of the current last name is the same as the previous. If it is, we keep moving by returning `null`, if not we return that new letter. The results is as follows:

<a href="https://www.loom.com/share/28e49d42cbe149579be15506a3e5f71e"> <p>Ionic Virtual Scroll w/ Headers - Watch Video</p> <img style="max-width:300px;" src="https://cdn.loom.com/sessions/thumbnails/28e49d42cbe149579be15506a3e5f71e-with-play.gif"> </a>

## Conclusion

That’s it! You now have a virtual list, with smooth scrolling, broken into sections by the contact’s last name. There are obviously countless ways to extend this further for your needs. One way would be to create a clickable index to jump between these sections. Maybe that will be in another tutorial 🙂

To find the full code for this demo, click [here](https://github.com/connersimmons/ionic-vs-example).