typescriptprimengangular12

PrimeNG DataTable with a skeleton effect instead of loading circle


How do I make a DataTable (PrimeNG) use the skeleton loader? If you look at the following docs link, this is how my current loading effect looks like.

PrimeNG actually implemented that skeleton effect themselves here, but I can't really find StackBlitz or code snippet there.

view-lecturers.component.ts

import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Lecturer } from '../lecturer';
import { LecturerService } from '../lecturer.service';
import { ConfirmationService, MessageService, PrimeNGConfig } from 'primeng/api';
import { BreadcrumbService } from '@core/services';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { Table } from 'primeng/table';

@Component({
  selector: 'app-view-lecturers',
  templateUrl: './view-lecturers.component.html',
  providers: [MessageService, ConfirmationService]
})
export class ViewLecturersComponent implements OnInit, OnDestroy {

  lecturerDialog: boolean;
  lecturers: Lecturer[];
  lecturer: Lecturer;
  selectedLecturers: Lecturer[];
  submitted: boolean;
  loading: boolean = true;

  private componentDestroyed$ = new Subject<boolean>();

  @ViewChild('dt') table: Table;

  constructor(
    private lecturerService: LecturerService,
    private messageService: MessageService,
    private confirmationService: ConfirmationService,
    private breadcrumbService: BreadcrumbService,
    private primengConfig: PrimeNGConfig) {
    this.breadcrumbService.setItems([
      { label: 'Преподаватели' },
      { label: 'Преглед' }
    ]);
  }

  ngOnInit() {
    this.lecturerService.getLecturers()
      .pipe(takeUntil(this.componentDestroyed$))
      .subscribe(data => {
        setTimeout(() => {
          this.lecturers = data;
          this.loading = false;
        }, 1000);
      });

    this.primengConfig.ripple = true;
  }

  ngOnDestroy() {
    this.componentDestroyed$.next(true);
    this.componentDestroyed$.complete();
  }

  applyFilterGlobal($event: any) {
    this.table.filterGlobal(($event.target as HTMLInputElement).value, 'contains');
  }

  openNew() {
    this.lecturer = {};
    this.submitted = false;
    this.lecturerDialog = true;
  }

  deleteSelectedLecturers() {
    this.confirmationService.confirm({
      message: 'Сигурни ли сте, че искате да изтриете данните за избраните преподаватели?',
      header: 'Потвърждение',
      icon: 'pi pi-exclamation-triangle',
      acceptLabel: 'Да',
      rejectLabel: 'Не',
      accept: () => {
        this.lecturers = this.lecturers.filter(val => !this.selectedLecturers.includes(val));
        this.selectedLecturers = [];
        this.messageService.add({ severity: 'success', summary: 'Успешно', detail: 'Данните за преподавателите са изтрити', life: 3000 });
      }
    });
  }

  editLecturer(lecturer: Lecturer) {
    this.lecturer = { ...lecturer };
    this.lecturerDialog = true;
  }

  deleteLecturer(lecturer: Lecturer) {
    this.confirmationService.confirm({
      message: 'Сигурни ли сте, че искате да изтриете данните за ' + lecturer.displayName + '?',
      header: 'Потвърждение',
      icon: 'pi pi-exclamation-triangle',
      acceptLabel: 'Да',
      rejectLabel: 'Не',
      accept: () => {
        this.lecturers = this.lecturers.filter(val => val.id !== lecturer.id);
        this.lecturer = {};
        this.messageService.add({ severity: 'success', summary: 'Успешно', detail: 'Данните за преподавателя са изтрити', life: 3000 });
      }
    });
  }

  hideDialog() {
    this.lecturerDialog = false;
    this.submitted = false;
  }

  saveLecturer() {
    this.submitted = true;

    if (this.lecturer.fullName?.trim() && this.lecturer.displayName?.trim()) {
      if (this.lecturer.id) {
        this.lecturers[this.findIndexById(this.lecturer.id)] = this.lecturer;
        this.messageService.add({ severity: 'success', summary: 'Successful', detail: 'Данните за преподавателя са обновени', life: 3000 });
      }
      else {
        this.lecturers.push(this.lecturer);

        // update to what's in db instead

        this.messageService.add({ severity: 'success', summary: 'Successful', detail: 'Преподавателят е добавен', life: 3000 });
      }

      this.lecturers = [...this.lecturers];
      this.lecturerDialog = false;
      this.lecturer = {};
    }
  }

  findIndexById(id: number): number {
    let index = -1;
    for (let i = 0; i < this.lecturers.length; i++) {
      if (this.lecturers[i].id === id) {
        index = i;
        break;
      }
    }

    return index;
  }

}

view-lecturers.component.html

<div class="p-grid">
  <div class="p-col-12">

    <p-toast></p-toast>

    <div class="card">
      <p-toolbar styleClass="p-mb-4">
        <ng-template pTemplate="left">
          <button pButton pRipple label="Добавяне" icon="pi pi-plus" class="p-button-success p-mr-2 p-mb-2"
            (click)="openNew()"></button>
          <button pButton pRipple label="Изтриване" icon="pi pi-trash" class="p-button-danger p-mb-2"
            (click)="deleteSelectedLecturers()" [disabled]="!selectedLecturers || !selectedLecturers.length"></button>
        </ng-template>
      </p-toolbar>

      <p-table #dt [value]="lecturers" [(selection)]="selectedLecturers" dataKey="id" styleClass="p-datatable-lecturers"
        [rowHover]="true" [rows]="10" [showCurrentPageReport]="true" [rowsPerPageOptions]="[10,25,50]"
        [loading]="loading" [paginator]="true"
        currentPageReportTemplate="Показват се от {first} до {last} от общо {totalRecords} записа"
        [globalFilterFields]="['fullName','displayName']">
        <ng-template pTemplate="caption">
          <div class="p-d-flex p-flex-column p-flex-md-row p-jc-md-between table-header">
            <h5 class="p-m-0">Преподаватели</h5>
            <span class="p-input-icon-left">
              <i class="pi pi-search"></i>
              <input pInputText type="text" (input)="applyFilterGlobal($event)" placeholder="Търсене..." />
            </span>
          </div>
        </ng-template>
        <ng-template pTemplate="header">
          <tr>
            <th style="width: 3rem">
              <p-tableHeaderCheckbox></p-tableHeaderCheckbox>
            </th>
            <th pSortableColumn="fullName">Име <p-sortIcon field="fullName"></p-sortIcon>
            </th>
            <th pSortableColumn="displayName">Псевдоним <p-sortIcon field="displayName"></p-sortIcon>
            </th>
            <th></th>
          </tr>
        </ng-template>
        <ng-template pTemplate="body" let-lecturer>
          <tr>
            <td>
              <p-tableCheckbox [value]="lecturer"></p-tableCheckbox>
            </td>
            <td>
              {{lecturer.displayName}}
            </td>
            <td>
              {{lecturer.fullName}}
            </td>
            <td>
              <button pButton pRipple icon="pi pi-pencil" class="p-button-rounded p-button-success p-mr-2"
                (click)="editLecturer(lecturer)"></button>
              <button pButton pRipple icon="pi pi-trash" class="p-button-rounded p-button-warning"
                (click)="deleteLecturer(lecturer)"></button>
            </td>
          </tr>
        </ng-template>
        <ng-template pTemplate="summary">
          <div class="p-d-flex p-ai-center p-jc-between">
            Общо {{lecturers ? lecturers.length : 0 }} преподаватели.
          </div>
        </ng-template>
      </p-table>
    </div>

    <p-dialog [(visible)]="lecturerDialog" [style]="{width: '450px'}" header="Данни за преподавател" [modal]="true"
      styleClass="p-fluid">
      <ng-template pTemplate="content">
        <div class="p-field">
          <label for="fullName">Име</label>
          <input #fullName="ngModel" id="fullName" type="text" pInputText [(ngModel)]="lecturer.fullName"
            [ngClass]="{'ng-dirty': (fullName.invalid && submitted) || (fullName.dirty && fullName.invalid)}" required
            autofocus />
          <small class="p-error" *ngIf="(fullName.invalid && submitted) || (fullName.dirty && fullName.invalid)">Името е
            задължително.</small>
        </div>
        <div class="p-field">
          <label for="displayName">Псевдоним</label>
          <input #displayName="ngModel" id="displayName" type="text" pInputText [(ngModel)]="lecturer.displayName"
            [ngClass]="{'ng-dirty': (displayName.invalid && submitted) || (displayName.dirty && displayName.invalid)}"
            required />
            <small class="p-error" *ngIf="(displayName.invalid && submitted) || (displayName.dirty && displayName.invalid)">Псевдонимът е задължителен.</small>
        </div>
      </ng-template>

      <ng-template pTemplate="footer">
        <button pButton pRipple label="Отказ" icon="pi pi-times" class="p-button-text" (click)="hideDialog()"></button>
        <button pButton pRipple label="Запази" icon="pi pi-check" class="p-button-text"
          (click)="saveLecturer()"></button>
      </ng-template>
    </p-dialog>

    <p-confirmDialog [style]="{width: '450px'}"></p-confirmDialog>
  </div>
</div>

Solution

  • Add this template to your table. When the loading attribute is true, then it will show this template inside the table. Check the official documentation for the templates and the loadingbody template link

    <ng-template pTemplate="loadingbody">
        <tr>
          <td><p-skeleton></p-skeleton></td>
        </tr>
    </ng-template>