Category: Ionic

  • Angular Autolink Pipe

    In development of a social platform for work, I found myself needing to support the creation of links for commons items such as, hashtags, urls, phone numbers, emails, etc. First instinct was, someone definitely has a solution for this! Thankfully, that turned out to be true (h/t Greg Jacobs).

    I knew Autolinker.js could save me a ton of time, with little implementation efforts if I simply built it into an Angular pipe. So, using the library I built a custom class for my own personal settings I would need, then built out the pipe to call within the innerHTML of a paragraph tag. And… that’s really all it took!

    Preview of clickable “linkified” text.

    Implementation

    So, to give you a quick glimpse of how to do this yourself, I built out a quick Ionic Angular project using the list starter. The full code can be found here. To start your own, you can use the following:

    $ ionic start ionic-linkify list --type=ionic-angular
    $ cd ionic-linkify
    $ ionic serve

    Once that was ready, I did some work to convert the “message” data to “tweet” data. I thought grabbing some fake tweet data would be a good way to demonstrate the concept. I turned to Simon Grimm‘s fake tweet data he used in one of his recent Twitter UI tutorials, so I didn’t have to create my own. The conversion involved mocking up the interface to the Tweet objects and pasting in the static list of tweets from Simon’s endpoint into the service. I did this to keep the example simple and not build out the HttpClient logic necessary. There are plenty examples out there to build a quick-and-dirty API call. The service looks as follows:

    // data.service.ts
    ...
    export interface Tweet {
      username: string;
      handle: string;
      like: number;
      retweets: number;
      response: number;
      text: string;
      link?: string;
      date: string;
      liked: boolean;
      retweet: boolean;
      attachment?: string;
      img: string;
    }
    ...
    export class DataService {
      ...
      public tweets: Tweet[] = [
        {
          username: "Max Lynch",
          handle: "maxlynch",
          like: 446,
          retweets: 173,
          response: 21,
          text:
            "Crazy, according to @appfigures, @Ionicframework is one of the top 4 development SDKs on both iOS and Android, and one of the top three 3rd party SDKs (more like top 2.5 since it overlaps w/ Cordova) Rocket",
          date: "1581322823",
          liked: true,
          retweet: false,
          attachment:
            "https://devdactic.fra1.digitaloceanspaces.com/twitter-ui/attachement-0.jpeg",
          img:
            "https://devdactic.fra1.digitaloceanspaces.com/twitter-ui/twitter-max.jpg",
        },
        ...
      ];
    
      ...
    
      public getTweets(): Tweet[] {
        return this.tweets;
      }
    }
    

    Once that was ready to go, I created my Linkifier class to hold my “linkify” settings. The class to follow looks a touch different than the one I implemented into my work project’s build, but I wanted to keep it a more generic use case here:

    // linkifier.ts
    import { Autolinker, AutolinkerConfig, HashtagMatch } from "autolinker";
    
    const AUTOLINKER_CFGS: AutolinkerConfig = {
      urls: {
        schemeMatches: true,
        wwwMatches: true,
        tldMatches: true,
      },
      email: true,
      phone: true,
      mention: "twitter",
      hashtag: "twitter",
      stripPrefix: false,
      stripTrailingSlash: false,
      newWindow: true,
      truncate: {
        length: 0,
        location: "end",
      },
      decodePercentEncoding: true,
    };
    
    export class Linkifier {
      private autolinker: Autolinker;
    
      constructor() {
        this.autolinker = new Autolinker(AUTOLINKER_CFGS);
      }
    
      public link(textOrHtml: string): string {
        return this.autolinker.link(textOrHtml);
      }
    }
    

    After that was good to go, it was time to add in the pipe:

    // linkify.pipe.ts
    ...
    @Pipe({
      name: "linkify",
    })
    export class LinkifyPipe implements PipeTransform {
      private linkifer: Linkifier;
    
      constructor() {
        this.linkifer = new Linkifier();
      }
    
      transform(value: string): string {
        return this.linkifer.link(value);
      }
    }
    
    @NgModule({
      declarations: [LinkifyPipe],
      exports: [LinkifyPipe],
    })
    export class LinkifyPipeModule {}

    So, the pipe’s transform is quite simple. After the Linkifier is initialized in the constructor, holding all of the necessary logic behind the scenes, all that must be done is to return the value from calling the link function. As shown in the class above, this function merely takes in text or html and “linkifies” it based on the rules setup in the configuration. It then returns the appropriate HTML. This is why we’ll house the result in the innerHTML of our p tag so it renders properly as a link:

    <p [innerHTML]="tweet.text | linkify"></p>

    Alright, so after importing the LinkifyPipeModule defined above into the Tweet component module for use, and modify the tweet output, we get the result!

    Again, for a full copy of the code, you can navigate to the github repo!


  • Angular Masked Phone Number Input

    Many of ours apps at some point will require a form with user input. More often than not, a phone number input is required. Rather than simply have the digits, it can be helpful to format the number as its typed in. It’s much more visibly appealing and easier to find a mistake if there is one.

    To do this in a form will require a few steps. We will need a form input for the number. We will need a directive to listen for input changes. Finally, we will need a pipe to tap into the phone formatting itself.

    First things first is creating the telephone format pipe. To do this, we’ll need to import the libphonenumber-js package by Nikolay Kuchumov. The pipe will take a number or phone input and use the the United States phone formatting:

    // phone.pipe.ts
    import { Pipe, PipeTransform } from "@angular/core";
    import { AsYouType } from "libphonenumber-js";
    
    @Pipe({
      name: "phone",
    })
    export class PhonePipe implements PipeTransform {
      transform(phoneValue: number | string): string {
        let ayt = new AsYouType("US");
        try {
          ayt.input(phoneValue + "");
          return ayt.getNumber().formatNational();
        } catch (error) {
          console.log(error);
          return phoneValue;
        }
      }
    }

    We can see here that regardless of input type we’ll return a string. The inputted value will be me made into a string (if it’s not already) and used to create an AsYouType object. Then, we’ll return the value as the national phone number format. If there is an error along the way, the original phone number value will be returned instead.

    Next, we’ll set up the directive to use this phone number pipe. We will listen to a couple different changes to the input: model change event and the backspace event. On each of those events, the input value will be sent through the pipe for formatting. This will look as follows:

    // phone-mask.directive.ts
    import { Directive, HostListener } from "@angular/core";
    import { NgControl } from "@angular/forms";
    import { PhonePipe } from "../pipes/phone.pipe";
    
    @Directive({
      selector: "[phoneMask]",
    })
    export class PhoneMaskDirective {
      constructor(public ngControl: NgControl, private phonePipe: PhonePipe) {}
    
      @HostListener("ngModelChange", ["$event"])
      onModelChange(event) {
        this.ngControl.valueAccessor.writeValue(
          this.phonePipe.transform(event)
        );
      }
    
      @HostListener("keydown.backspace", ["$event"])
      keydownBackspace(event) {
        this.ngControl.valueAccessor.writeValue(
          this.phonePipe.transform(event.target.value)
        );
      }
    }

    Now we’ll create our form with a telephone input. But first, we’ll need to allow the page in which that form resides to use the directive and the pipe. So, we’ll create a module out of the directive, then import that module to the page’s module, as well as add the phone pipe as a provider for the form’s page. We’ll also need the ReactiveFormsModule too for later use.

    // phone-mask.directive.ts
    ...
    export class PhoneMaskDirective {
      ...
    }
    
    @NgModule({
      declarations: [PhoneMaskDirective],
      exports: [PhoneMaskDirective],
    })
    export class PhoneMaskDirectiveModule {}
    // home.module.ts
    ...
    import { PhonePipe } from "../phone.pipe";
    import { PhoneMaskDirectiveModule } from "../phone-mask.directive";
    
    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        ReactiveFormsModule,
        IonicModule,
        HomePageRoutingModule,
        PhoneMaskDirectiveModule,
      ],
      declarations: [HomePage],
      providers: [PhonePipe],
    })
    export class HomePageModule {}

    With those tools usable, we’ll create the input on the page and add the directive to mask the inputted value:

    // home.page.html
    ...
    <ion-content [fullscreen]="true">
      <form [formGroup]="form" (ngSubmit)="submit()">
        <ion-input
          type="tel"
          formControlName="phone"
          placeholder="Phone Number"
          clearInput="true"
          autofocus="true"
          inputmode="tel"
          phoneMask
          (keyup.enter)="submit()"
        >
        </ion-input>
      </form>
    </ion-content>

    This input is inside of a form. The form here is mainly just to show you how to get the digits back on submit. The type of the ion-input is set to “tel”, as well as the input mode. I’ve done this so when on a mobile device the only tappable buttons should be numbers. There is a keyup.enter event so we can hit the submit fuction on enter button. Also, of course, we have the directive.

    The typescript side of this will look as follows:

    // home.page.ts
    import { Component } from "@angular/core";
    import { FormBuilder, FormGroup } from "@angular/forms";
    
    @Component({
      selector: "app-home",
      templateUrl: "home.page.html",
      styleUrls: ["home.page.scss"],
    })
    export class HomePage {
      public form: FormGroup;
    
      constructor(private formBuilder: FormBuilder) {
        this.form = this.formBuilder.group({
          phone: [""],
        });
      }
    
      public submit() {
        console.log(`Phone: ${this.digitsOnly(this.f.phone.value)}`);
      }
    
      get f() {
        return this.form.controls;
      }
    
      private digitsOnly(str: string) {
        return str.replace(/\D/g, "");
      }
    }

    The above code shows how to set up the form using FormGroup and FormBuilder. Then have helper functions to get to the form controls, as well as get the digits form the masked phone string. All of this considered, we get the following result:

    To get a full copy of the code, you can visit the repository here.


  • 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.


  • Login Form with Show/Hide Password Switch

    For me, I always appreciate when login forms give you an option to show the password I’ve typed in. It may seem commonplace, but all too often sites neglect to add this feature. It’s nothing more than a slight nuisance, but I appreciate being able to hit the toggle, show what I’ve typed so I don’t feel like I need to re-type everything if I think I’ve messed up. So, whenever I create an email/password login for my work projects, I make sure to include this feature.

    This is a very simple addition to a basic login form. First we setup our project, create a login page, and change the routing to go to this login page first. Of course, in a different terminal tab, we’ll serve the app to view realtime changes:

    $ ionic start ionic-login-hide-show blank --type=ionic-angular
    $ cd ionic-login-hide-show
    $ ionic g page login
    
    $ ionic serve

    Changing the first page to be the newly created login page requires modifying the app-routing.module.ts file as follows:

    // app-routing.module.ts
    ...
    const routes: Routes = [
      {
        path: "home",
        loadChildren: () =>
          import("./home/home.module").then((m) => m.HomePageModule),
      },
      {
        path: "login",
        loadChildren: () =>
          import("./login/login.module").then((m) => m.LoginPageModule),
      },
      {
        path: "",
        redirectTo: "login",
        pathMatch: "full",
      },
    ];
    ...

    After opening the newly created login.page.ts file, we’ll start to build out the form logic. This will include elements of the ReactiveFormsModule, so that will need to be imported to the login.module.ts file. We’ll build out a FormGroup with email and password fields, with some Validators to ensure users enter the correct information:

    // login.module.ts
    ...
    import { FormsModule, ReactiveFormsModule } from "@angular/forms";
    ...
    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        ReactiveFormsModule,
        IonicModule,
        LoginPageRoutingModule,
      ],
      declarations: [LoginPage],
    })
    export class LoginPageModule {}
    
    // login.page.ts
    import { Component, OnInit } from "@angular/core";
    import { FormBuilder, FormGroup, Validators } from "@angular/forms";
    ...
    export class LoginPage implements OnInit {
      loginForm: FormGroup;
    
      constructor(private formBuilder: FormBuilder) {
        this.loginForm = this.formBuilder.group({
          email: ["", Validators.compose([Validators.required, Validators.email])],
          password: ["", Validators.required],
        });
      }
    ...
    }

    Now that we have some form logic built out, we can place some HTML in place to get those inputs ready for the user to enter their email and password. Further, we’ll add a button for them to click to initiate the login process:

    <ion-header>
      <ion-toolbar>
        <ion-title>Login</ion-title>
      </ion-toolbar>
    </ion-header>
    
    <ion-content>
      <ion-card>
        <form [formGroup]="loginForm">
          <ion-item>
            <ion-label position="stacked">
              <ion-icon name="person-outline"></ion-icon>
              Email
            </ion-label>
            <ion-input type="email" formControlName="email">
            </ion-input>
          </ion-item>
          <ion-item >
            <ion-label position="stacked">
              <ion-icon name="lock-closed-outline"></ion-icon>
              Password
            </ion-label>
            <ion-input type="password" formControlName="password">
            </ion-input>
          </ion-item>
          <ion-button expand="block" type="submit">
            Login
            <ion-icon name="log-in-outline"></ion-icon>
          </ion-button>
        </form>
      </ion-card>
    </ion-content>

    Now that we have all of that taken care of, we can get to the important part, which is the icon to show and hide the password. Inside of the password item, we’ll add an icon, which has an on click action of toggling the password’s visibility. This function entails toggling a boolean for showing the password or not. Further, it will control which icon is shown. However, that won’t get the job done. We’ll also need to add some logic in the HTML to change what type of input the password field is. The type will be determined by the aforementioned boolean. Without this piece, we wouldn’t be able to actually visibly see the password at all. The other pieces are nice and all, but changing the input type is what really matters.

    // login.page.html
    ...
    <ion-item >
      <ion-label position="stacked">
        <ion-icon name="lock-closed-outline"></ion-icon>
        Password
      </ion-label>
      <ion-input
        [type]="showPwd ? 'text' : 'password'"
        formControlName="password"
      >
      </ion-input>
      <ion-icon
        slot="end"
        [name]="pwdIcon"
        (click)="togglePwd()"
      >
      </ion-icon>
    </ion-item>
    ...
    // login.page.ts
    ...
    export class LoginPage implements OnInit {
      loginForm: FormGroup;
      pwdIcon = "eye-outline";
      showPwd = false;
    ...
      togglePwd() {
        this.showPwd = !this.showPwd;
        this.pwdIcon = this.showPwd ? "eye-off-outline" : "eye-outline";
      }
    }

    So, with that, we can see that the password field type changes from 'text' to 'password' depending on the boolean value. The icon changes along with it to show you which type you’d be changing the field too. One thing to note is the the showPwd field should start as false. This way, the user gets what you’d expect first… the password is hidden. All together, it looks like this:

    Some notes

    There are certainly some improvements that could be made to this form, such as:

    • Showing text below the form fields indicating any errors.
    • Disabling the Login button unless the form is valid.
    • Not showing the show/hide button until a password has been typed.

    Let me know if that is something you’d like me to add in a part two!

    To get a copy of the full code example, check out the Github repo here.


  • Ionic Virtual Scroll with Headers

    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 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 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:

    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:

    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:

    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.

    A more updated version of this post can be found here.