Alphabetical Index with Ionic Virtual Scroll

This post is an extension of the first post on a contacts lists with headers to break up the sections. You can read that here!

I wanted to extend that example to show how we could add an alphabetical index on the side of the list. This was something that plagued me for a while. Then a quick Twitter conversation sparked my interest on it again. I wanted to solve this because I needed it for work and the current solution I had didn’t make me happy. The example I always referenced was Ross Martin’s ionic2-alpha-scroll. However, this needed to be modified a bit with newer versions of Ionic. That same Twitter conversation gave me a hint and I rolled with it.

My colleague Stephen and I came up with the ion-list pinned to side for that work project, but I wanted to bring it to everybody because I’m sure its gotta help someone! I searched for examples of something like this so many times. I’m sure this could help somebody out there. So, for this example, I started by adding the ion-list after the ion-virtual-scroll in the code. Then added in the styling Stephen wrote:

//home.page.html
<ion-header>
  <ion-toolbar>
    <ion-title> Contacts </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-virtual-scroll #vScroll [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-list #alphaList class="ion-alpha-sidebar">
    <ion-item
      *ngFor="let letter of alphabet"
      (click)="goLetter(letter)"
      lines="none"
    >
      {{letter}}
    </ion-item>
  </ion-list>
</ion-content>
// home.page.scss
.ion-alpha-sidebar {
  position: fixed;
  right: 0px;
  top: 50px;
  bottom: 0px;
  display: flex;
  flex-flow: column;
  z-index: 50000;
  margin: 0px;
  ion-item {
    font-size: 16px;
    color: #ffffff;
    flex: 1 1 auto;
    display: flex;
    list-style: none;
    width: 60px;
    font-weight: 500;
    text-align: center;
    align-items: center;
    justify-content: center;
    cursor: pointer;
  }
}

@media screen and (max-width: 1024px) {
  .ion-alpha-sidebar {
    top: 50%;
    right: 0;
    transform: translate(0, -50%);
    padding: 0px;
    ion-item {
      width: auto;
      font-size: 14px;
      color: var(--ion-color-primary);
    }
  }
}

You can see from the HTML above that creating the alphabetical index is done by looping over an array holding the letters of the alphabet. The array is created by a for loop iterating over the proper character codes representing those letters. There is a click event attached to each of those letters to jump to the position in the corresponding ion-virtual-scroll list. The code for creating the alphabet, as well as the code for jumping to the appropriate section by letter, looks as follows:

//home.page.ts
...
export class HomePage implements OnInit, AfterViewInit {
  
  @ViewChild(IonContent) content: IonContent;
  @ViewChild("vScroll") public virtualScroll: IonVirtualScroll;

  public contacts: Array<Contact> = new Array<Contact>();
  public alphabet: String[] = [];
  ...

  constructor(private contactsService: ContactsService) {
    this.alphabet.push(String.fromCharCode(35));
    for (let i = 0; i < 26; i++) {
      this.alphabet.push(String.fromCharCode(65 + i));
    }
  }
  ...
  goLetter(letter: string) {
    const firstContact = this.contacts.find((c) => {
      return c.name.last.toUpperCase().charAt(0) === letter.toUpperCase();
    });
    const wantedIndex = this.virtualScroll.items.findIndex(
      (item) => item === firstContact
    );
    this.virtualScroll.positionForItem(wantedIndex).then((offset: number) => {
      this.content.scrollToPoint(0, offset);
    });
  }
  ...
}

So, the previous code first adds a # to the alphabet for any contact sorting starting with a numeric. Then, adding each letter thereafter, starting with A (represented by character code 65). We then have the function to jump inside the list. It finds the first contact in the sorted contacts array where the letter matches the first letter of the last name (in my case). Then it finds the index of that contact within the virtual list. Followed by scrolling the ion-virtual-scroll to that specific index.

That’s pretty much all ya need for the side index!

Group Headings Revisited

In the previous post, linked at the top, I talked about how to create the section headers. However, since then, I’ve updated the code to be a bit more effective:

//home.page.ts
...
myHeaderFn = (record, recordIndex, records) => {
  let result = null;
  if (recordIndex !== 0) {
    const prevRec = records[recordIndex - 1];
    const currRec = record;
    const prevName = prevRec.name.last;
    const currName = currRec.name.last;
    if (prevName !== null && currName !== null) {
      let prevCharCode = prevName.toUpperCase().charCodeAt(0);
      let currCharCode = currName.toUpperCase().charCodeAt(0);
      if (prevCharCode !== currCharCode) {
        let prevChar = prevName.toUpperCase().charAt(0);
        let currChar = currName.toUpperCase().charAt(0);
        let prevIsLetter = this.isLetter(prevChar);
        if (!prevIsLetter) {
          let currIsLetter = this.isLetter(currChar);
          result = currIsLetter ? currName.toUpperCase().charAt(0) : null;
        } else {
          result = currName.toUpperCase().charAt(0);
        }
      }
    }
  } else {
    const name = record.name.last;
    if (name !== null) {
      let nameChar = name.toUpperCase().charAt(0);
      let headerChar = this.isLetter(nameChar) ? nameChar : "#";
      result = headerChar.toUpperCase();
    }
  }
  return result;
};

public isLetter(char: any): boolean {
  return /[a-zA-Z]/.test(char);
}
...

I now used the similar character code approach within the header function to create the alphabet. Using charCodeAt, we can compare two records using their number value. If we’re looking at the first index in the alphabetically sorted list, we simply set the header as #, if it’s a number, or the first character, if it’s a letter. Then, for the remaining list, we compare the number values. If they are not the same and the previous record was a number, we look at the current record’s first letter. If that’s a number, we leave the return as null. If it’s a letter, we return that letter. If the original character codes were not equal and the previous record started with a letter, then we simply return the current record’s first letter. It seems a bit complex, but isn’t too bad when you read through it a couple of times.

Maybe you have an even slicker solution!? If you do, I’d love to see it!

To get a copy of the source code, you can go here.