Κυριακή 5 Απριλίου 2020

Display paginated data chronologically with Angular (infinite scrolling)





  •  Display of data elements by time order. 
  • Scrolling down triggers month change in upper bar.
  • Clicking on a month triggers display of related elements




Prerequisites:

- Your backend should provide paginated data (see here how to)
- @angular/cdk": "~8.2.3" 
- @angular/material v8.2.3







1. Component - html



<div class="element-list " fxLayout="column">
  <div class='month-navigation'>
    <a class='monthView' *ngFor="let monthName of months; let i = index"
       (click)="monthClickAction(i)"  [style.left]="defineMoveToLeft(i)">
      {{monthName}}
    </a>
  </div>

  <cdk-virtual-scroll-viewport mat-list itemSize="60" minBufferPx="240" maxBufferPx="480"
    class="transaction-scrolling-viewport" role="list" (scrolledIndexChange)=scrollIndexChangeAction($event) fxFlex="grow">

<!-- See step 5 -->
  <jhi-element-entry *cdkVirtualFor="let element of elementsToShow"
  [icon]="'grade'" [date]="element.date" [amount]="element.amount"
  [mainText]="element.elementName">
  </jhi-element-entry>

    <div class="load-item" >
      <div class="content">
        <div class="text">Loading elements..</div>
        <mat-progress-bar mode="indeterminate"></mat-progress-bar>
      </div>
    </div>

  </cdk-virtual-scroll-viewport>


</div>



2. Component - ts


import {Component, Input, OnInit, ViewChild} from '@angular/core';
import {FormBuilder} from '@angular/forms';
import {IElement} from '../model/your-element.model';
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {YourService} from '../services/yourApp.service';
import {HttpErrorResponse, HttpResponse} from '@angular/common/http';
import {HttpPage} from '../model/page.model';

@Component({
  selector: 'jhi-element-list',
  templateUrl: './element-list.component.html',
  styleUrls: ['./element-list.component.scss']
})
export class ElementListComponent implements OnInit {
  @Input()
  public showMonthBar = true;
// See step 7
  private elements: IElement[] = [];
  public elementsToShow: IElement[] = [];

  // See step 4 for this page model
  private page: HttpPage = {number: 0, size: 40, totalPages: 0, totalElements: 0};

  private readonly curMonth: number;
  private readonly curYear: number;
  private activeYear: number;
  indexOfActiveMonth: number;
  private lastIndex = 0;

  @ViewChild(CdkVirtualScrollViewport, {static: true})
  scrollViewPort!: CdkVirtualScrollViewport;

  months = [
    'january',
    'february',
    'march',
    'april',
    'may'
  ];

  constructor(
    private fb: FormBuilder,
    private yourService: YourService
  ) {
    const date = new Date();

    this.curMonth = date.getMonth();
    this.curYear = date.getFullYear();
    this.activeYear = date.getFullYear();
    this.indexOfActiveMonth = date.getMonth();
  }

  ngOnInit(): void {
    this.getElements(0, false);
  }

  getElements(pageNumber: number, doIncrement: boolean): void {
// See step 6
    this.yourService.getElementsByPageAndSize(pageNumber, this.page.size).subscribe(
      (res: HttpResponse<any>) => {
        if (doIncrement) {
// Your API must return paginated data - see this tutorial
          this.elements = this.elements.concat(res.body._embedded.elementDTOList);
        } else {
          this.elements = res.body._embedded.elementDTOList;
        }

        this.page = res.body.page;
        this.elements = this.elements.sort((t1, t2) => t2.date.getTime() - t1.date.getTime());
        this.elementsToShow = this.elements;
      },
      (error: HttpErrorResponse) => {
        // handle error...
      }
    );
  }

  scrollIndexChangeAction(index: number): void {
    if (this.elements[index] === undefined) return;
    this.updateMonthByIndex(this.elements[index].date.getMonth());
    // Load on down scroll event
    if (
      this.lastIndex < index && this.scrollViewPort.measureScrollOffset('bottom') < 50) {
      this.loadNextElementsBatch();
    }
    this.lastIndex = index;
  }

  monthClickAction(index: number): void {
    let compareYear = this.activeYear;
    if (index === 11) {
      compareYear--;
    }
    if (index === 0) {
      compareYear++;
    }

    if (compareYear === this.curYear && index > this.curMonth) return;

    const viewportIndex = this.elements.findIndex(el => el.date.getMonth() === index);
    if (viewportIndex === -1) {
      this.loadNextElementsBatch();
      return;
    }

    this.scrollViewPort.scrollToIndex(viewportIndex, 'smooth');
  }

  updateMonthByIndex(index: number): void {
    if (index === 11 && this.indexOfActiveMonth === 0) {
      this.activeYear--;
    }
    if (index === 0 && this.indexOfActiveMonth === 11) {
      this.activeYear++;
    }
    this.indexOfActiveMonth = index;
  }

  loadNextElementsBatch(): void {
    this.scrollViewPort.checkViewportSize();

    setTimeout(() => this.scrollViewPort.scrollTo({bottom: 0, behavior: 'smooth'}), 25);
    this.getElements(this.page.number + 1, true);
  }

  defineMoveToLeft(index: number): string {
    if (this.indexOfActiveMonth === 0 && index === this.months.length - 1) {
      return '0';
    }
    if (this.indexOfActiveMonth === this.months.length - 1 && index === 0) {
      return '66%';
    }
    if (this.indexOfActiveMonth === undefined) {
      return 33 * index + '%';
    }
    return 33 * (index - this.indexOfActiveMonth + 1) + '%';
  }

}




3. Component - scss



.element-list {

  padding-left: 15px;
  padding-right: 15px;
  height: 98%;
  width: 100%;

  .month-navigation {
    width: 100%;
    position: relative;
    height: 4.8vh;
    margin: 2vh 0;
    overflow: hidden;

    .monthView {
      display: block;
      position: absolute;
      left: 33%;
      transition: 0.2s ease-in-out left;
      top: 0;
      text-align: center;
      line-height: 3.5vh;
      font-size: 1.7vh;
      width: 33%;
      color: #aaa;

      &.active {
        color: #000;
      }
      &.active:after {
        content: '';
        display: block;
        margin: 0 auto;
        width: 50%;
        border-bottom: 0.3vh solid #000;
      }
    }
  }

  mat-form-field.mat-form-field {
    display: block;
    font-size: 2.08vh;
    line-height: 2.6vh;
  }

  .transaction-scrolling-viewport {
    height: 100%;
    width: 100%;
  }

  .load-item {
    width: 98%;

    &.bordered {
      border-bottom: 1px solid #aaa;
    }

    .content {
      width: 80%;
      margin: 1.5vh 10%;
      .text {
        color: #aaa;
        font-size: 1.6vh;
        text-align: center;
        line-height: 2.5vh;
      }
    }
  }

}




4. Model.ts for page property


  export interface HttpPage {
    size: number;
    totalElements: number;
    totalPages: number;
    number: number;
  }



5. Create inner/entry element

Create/adjust yourself this component with date,amount and mainText @input fields, or see this tutorial (coming soon)



6. Create a service method to get paginated data

  
getElementsByPageAndSize(page: number, size: number): Observable<HttpResponse<IElement[]>> {
    let params = new HttpParams();
    httpParams = httpParams .append('page', page.toString());
    httpParams = httpParams .append('size', size.toString());
    return this.http.get<IElement[]>(YOUR_SERVER_API_URL + 'api/your/elements', {
      params: httpParams ,
      observe: 'response'
    });
  }


7. IElement model

export interface IElement {
  elementName: string;
  amount: number;
  date: Date;
}

Δεν υπάρχουν σχόλια:

Δημοσίευση σχολίου

What may be missing, or could get better?