Support Dynamic Multiple Columns And Infinity Scroll in Ionic Angular
Tags: ionic, angular, performance
Published: 2021-02-08 07:20:00

Hello friends,
In my latest Project, I Found My product list is so large and it’s slow on response. So I was going to use a virtual scroll, But it does not support more than one column or dynamic multiple columns.
So I tried to found a way to make ion-virtual-scroll to support more than one column and maybe dynamic columns.

ion-virtual-scroll has an item list like [items]=”list”, and it’ll iterate over the list. To support multiple columns we must find how many items is in the list and calculate their index, After that we can use virtual-list index to retrieve data from the list.
As you can see below, Extra columns will be added to the virtual list. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/va2jy3jc4ht8pgs0urqd.gif)
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ozj8pn7fxs8gsre7i6iq.gif)

#### Let’s do it!
First Create an ionic angular project and then install faker for mocking data.
```cli
ionic start virtual-columns tabs
```
`npm install faker --save`
`npm install @types/faker --save`

inside your .ts file import faker
```typescript
import * as faker from 'faker';
```

Get List of Data from faker package:

```typescript
getEmployees() {
    for (let i = 0; i < 250; i++) {
      this.dataList.push({
        id: this.dataList.length,
        image: faker.image.image(),
        name: faker.name.firstName(),
        address: faker.address.streetAddress(),
        intro: faker.lorem.words()
      });
    }
    // for triggering pipe when data source changed //
    this.nextPipe = this.dataList.length;
  }
```

Listen to Screen resize and calculate how many extra column must add to the list:
```typescript
@HostListener('window:resize', ['$event'])
  getScreenSize(event?) {
    this.screenWidth = window.innerWidth;
    this.exteraCol = Math.trunc(this.screenWidth / this.vColMinWidth) -1;
    this.exteraCol = this.exteraCol < 0 ? 0 : this.exteraCol;
    this.exteraCol = this.exteraCol > 3 ? 3 : this.exteraCol; // if we want to have max virtual column count
  }
```
Import virtualScroll and infinity ViewChild to .ts file, To detect and manage when scroll touch end.

```typescript
export class Tab3Page {
  @ViewChild(IonVirtualScroll) virtualScroll: IonVirtualScroll;
  @ViewChild(IonInfiniteScroll) infiniteScroll: IonInfiniteScroll;
...
```
Make it infinity scroll:
```typescript
loadData(event: any) {
    setTimeout(() => {

      // load more data
      this.getEmployees();
      this.virtualScroll.checkEnd();  // trigger end of virtual list
      event.target.complete();

      if (this.dataList.length === 1000) {
        event.target.disabled = true;
      }
    }, 500);
  }
```
We must to know Min Width of virtual columns, so set vColMinWidth as much as you need.
* For me it is vColMinWidth = 360;

Generate a pipe with ionic cli command

`ionic generate pipe pipes/virtual-list-index`

Add VirtualListIndexPipe to PipesModule
Fetch item’s index of dataList with pipe like this:
```typescript
export class VirtualListIndexPipe implements PipeTransform {

  transform(array: Array<any>, exteraCol: number, nextPipe: number): number[][] {
    const range = (s: number, e: number) => e > s ? [s, ...range(s + 1, e)] : [s];
    const virtualList = [] as number[][];

    let i = 0;
    for (i; i < (array.length - exteraCol); i += exteraCol + 1){
      virtualList.push([...range(i, i + exteraCol)]);
    }
    // last 2d array maybe not full lenght
    virtualList.push([...range(i, array.length - 1)]);

    return virtualList;
  }
}
```
Import PipesModule to your component module, And use that pipe in your virtual scroll items like this:

```html
<ion-virtual-scroll approxItemHeight="62.4px" [itemHeight]="itemHeightFn"
  [items]="dataList | virtualListIndex: exteraCol : nextPipe">
```

Now our virtual list items has the value of datalist items refrence and we can use it to get data from dataList
```html
<div *virtualItem="let item;let i = index">
    <ion-row class="ion-nowrap>
        <ion-col *ngFor="let j of item;">
            <h3>i = {{dataList[j].id}} </h3>
        </ion-col>
    </ion-row>
</div>
```
Complete File's should be like this:

#### tab3.ts File
```typescript
import { Component, ViewChild } from '@angular/core';
import { IonInfiniteScroll, IonVirtualScroll } from '@ionic/angular';
import * as faker from 'faker';
import { HostListener } from '@angular/core';

@Component({
  selector: 'app-tab3',
  templateUrl: 'tab3.page.html',
  styleUrls: ['tab3.page.scss']
})
export class Tab3Page {
  @ViewChild(IonVirtualScroll) virtualScroll: IonVirtualScroll;
  @ViewChild(IonInfiniteScroll) infiniteScroll: IonInfiniteScroll;

  dataList = [];
  vColMinWidth = 360; // virtual list columns min width as pixel
  exteraCol = 0;  // how many columns should be add to virtual list
  nextPipe = 0;
  screenWidth: number;

  constructor() {
    this.getScreenSize();
    this.getEmployees();
  }

  @HostListener('window:resize', ['$event'])
  getScreenSize(event?) {
    this.screenWidth = window.innerWidth;
    this.exteraCol = Math.trunc(this.screenWidth / this.vColMinWidth) -1;
    this.exteraCol = this.exteraCol < 0 ? 0 : this.exteraCol;
    this.exteraCol = this.exteraCol > 3 ? 3 : this.exteraCol; // if we want to have max virtual column count
  }

  getEmployees() {
    for (let i = 0; i < 250; i++) {
      this.dataList.push({
        id: this.dataList.length,
        image: faker.image.image(),
        name: faker.name.firstName(),
        address: faker.address.streetAddress(),
        intro: faker.lorem.words()
      });
    }
    this.nextPipe = this.dataList.length;
  }

  itemHeightFn(item, index) { // better performance if setting item height
    return 62.4;
  }

  loadData(event: any) {
    setTimeout(() => {

      // load more data
      this.getEmployees();
      this.virtualScroll.checkEnd();  // trigger end of virtual list
      event.target.complete();

      if (this.dataList.length === 1000) {
        event.target.disabled = true;
      }
    }, 500);
  }
}

```
#### Tab3PageModule:
```typescript
import { PipesModule } from '../pipes/pipes.module';

@NgModule({
  imports: [
    PipesModule,
...
```
#### tab3.html file
```typescript
<ion-content [fullscreen]="true" class="ion-padding">
  <ion-virtual-scroll approxItemHeight="62.4px" [itemHeight]="itemHeightFn"
    [items]="dataList | virtualListIndex: exteraCol : nextPipe">
    <div *virtualItem="let item;let i = index">
      <ion-row class="ion-nowrap>
        <ion-col *ngFor="let j of item;">
          <h3>i = {{dataList[j].id}} </h3>
        </ion-col>
      </ion-row>
    </div>
  </ion-virtual-scroll>

  <ion-infinite-scroll threshold="150px" (ionInfinite)="loadData($event)">
    <ion-infinite-scroll-content loadingSpinner="bubbles" loadingText="Loading more data...">
    </ion-infinite-scroll-content>
  </ion-infinite-scroll>
</ion-content>
```
#### VirtualListIndexPipe:
```typescript
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'virtualListIndex'
})
export class VirtualListIndexPipe implements PipeTransform {

  transform(array: Array<any>, exteraCol: number, nextPipe: number): number[][] {
    const range = (s: number, e: number) => e > s ? [s, ...range(s + 1, e)] : [s];
    const virtualList = [] as number[][];

    let i = 0;
    for (i; i < (array.length - exteraCol); i += exteraCol + 1){
      virtualList.push([...range(i, i + exteraCol)]);
    }
    // last 2d array maybe not full lenght
    virtualList.push([...range(i, array.length - 1)]);

    return virtualList;
  }
}
```
#### [try to resize screen from stackblitz](https://stackblitz.com/edit/ionic-angular-v5-by5rkf?file=src%2Fapp%2Fapp.component.html)

It was useful for my project, I hope you liked it 😏