Intégration web angularforall.com

- PrimeNG Table : tri, filtres, pagination avancés

Primeng P-Table Angular Datatable Tri-Colonnes Filtres-Table Pagination Lazy-Loading Virtual-Scroll Export-Csv Angular-Standalone Ui-Library Front-End Integration-Web
PrimeNG Table : tri, filtres, pagination avancés

Maîtrisez p-table de PrimeNG : tri multi-colonnes, filtres globaux, pagination côté client et serveur, lazy load, virtual scroll et export CSV en Angular 17+.

Pourquoi p-table ?

<p-table> est le composant phare de PrimeNG pour Angular. Il combine en un seul tag les fonctionnalités habituellement dispersées dans plusieurs librairies : tri multi-colonnes, filtrage par colonne et global, pagination client/serveur, sélection (single/multi/checkbox), édition inline, virtual scroll, export, drag & drop des colonnes, redimensionnement et persistance d'état.

Avec Angular 17+ et PrimeNG 17+, le composant fonctionne en mode standalone : un simple imports: [TableModule] dans votre composant suffit, sans NgModule intermédiaire.

Comparatif rapide des options

Besoin Propriété p-table Quand l'activer
Tri sur 1 colonnesortMode="single" (défaut)Cas standard
Tri multi-colonnessortMode="multiple"Tableaux analytiques
Pagination client[paginator]="true"< 1000 lignes en mémoire
Pagination serveur[lazy]="true"> 1000 lignes / API paginée
10k+ lignes fluides[virtualScroll]="true"Logs, séries temporelles
Filtre global[globalFilterFields]Recherche transverse
PrimeNG 17+ : Toutes les directives pSortableColumn, pTemplate et le module TableModule sont compatibles standalone. Plus besoin de BrowserAnimationsModule au niveau racine — utilisez provideAnimations() dans app.config.ts.

Installation et imports standalone

Installer PrimeNG et les dépendances

# PrimeNG core + icônes + thème par défaut
npm install primeng primeicons

# Animations Angular (requis pour les transitions p-table)
npm install @angular/animations

Configurer les animations dans app.config.ts

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAnimations(),       // requis par p-table (animations de tri/expand)
    provideHttpClient(),       // requis pour le mode lazy
  ],
};

Importer le thème CSS dans angular.json

// angular.json — section "styles"
"styles": [
  "node_modules/primeng/resources/themes/lara-light-blue/theme.css",
  "node_modules/primeng/resources/primeng.min.css",
  "node_modules/primeicons/primeicons.css",
  "src/styles.css"
]

Premier composant avec p-table

// users-table.component.ts
import { Component, signal } from '@angular/core';
import { TableModule } from 'primeng/table';

interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'editor' | 'viewer';
  active: boolean;
}

@Component({
  selector: 'app-users-table',
  standalone: true,
  imports: [TableModule],
  template: `
    <p-table [value]="users()" [paginator]="true" [rows]="10">
      <ng-template pTemplate="header">
        <tr>
          <th>Nom</th>
          <th>Email</th>
          <th>Rôle</th>
          <th>Actif</th>
        </tr>
      </ng-template>
      <ng-template pTemplate="body" let-u>
        <tr>
          <td>{{ u.name }}</td>
          <td>{{ u.email }}</td>
          <td>{{ u.role }}</td>
          <td>{{ u.active ? 'Oui' : 'Non' }}</td>
        </tr>
      </ng-template>
    </p-table>
  `,
})
export class UsersTableComponent {
  users = signal<User[]>([
    { id: 1, name: 'Alice Martin',  email: 'alice@demo.fr',  role: 'admin',  active: true  },
    { id: 2, name: 'Bruno Dupont',  email: 'bruno@demo.fr',  role: 'editor', active: true  },
    { id: 3, name: 'Clara Lefevre', email: 'clara@demo.fr',  role: 'viewer', active: false },
  ]);
}
Dans PrimeNG 17+, pTemplate reste la directive officielle pour cibler les zones header, body, footer, caption, summary, emptymessage et paginatorleft / paginatorright.

Tri simple, multi-colonnes, custom

Tri simple — pSortableColumn

<p-table [value]="products()" sortMode="single">
  <ng-template pTemplate="header">
    <tr>
      <th pSortableColumn="name">
        Nom <p-sortIcon field="name"></p-sortIcon>
      </th>
      <th pSortableColumn="price">
        Prix <p-sortIcon field="price"></p-sortIcon>
      </th>
      <th pSortableColumn="stock">
        Stock <p-sortIcon field="stock"></p-sortIcon>
      </th>
    </tr>
  </ng-template>
  <ng-template pTemplate="body" let-p>
    <tr>
      <td>{{ p.name }}</td>
      <td>{{ p.price | currency:'EUR' }}</td>
      <td>{{ p.stock }}</td>
    </tr>
  </ng-template>
</p-table>

Tri multi-colonnes (touche Maj enfoncée)

<p-table
  [value]="products()"
  sortMode="multiple"
  [multiSortMeta]="initialSort">
  <!-- même structure header/body que ci-dessus -->
</p-table>
// products-list.component.ts
import { SortMeta } from 'primeng/api';

export class ProductsListComponent {
  // Tri initial : par catégorie ASC puis prix DESC
  initialSort: SortMeta[] = [
    { field: 'category', order: 1  },
    { field: 'price',    order: -1 },
  ];
}

Tri personnalisé — événement (sortFunction)

<p-table
  [value]="orders()"
  [customSort]="true"
  (sortFunction)="customSort($event)">
  <!-- ... -->
</p-table>
customSort(event: any): void {
  // event.data : tableau à trier — event.field : champ — event.order : 1 ou -1
  event.data.sort((a: any, b: any) => {
    // Tri spécial pour le champ "priority" : critical > high > normal > low
    const priorities = { critical: 4, high: 3, normal: 2, low: 1 };
    const va = priorities[a[event.field] as keyof typeof priorities] ?? 0;
    const vb = priorities[b[event.field] as keyof typeof priorities] ?? 0;
    return (va - vb) * event.order;
  });
}
Tri sur dates : p-table compare les valeurs avec l'opérateur <. Les chaînes ISO ("2026-05-09") se trient correctement par ordre alphabétique. Pour des objets Date, utilisez customSort et comparez via .getTime().

Filtres : par colonne et global

Filtre global sur plusieurs champs

<div class="d-flex justify-content-end mb-2">
  <span class="p-input-icon-left">
    <i class="pi pi-search"></i>
    <input
      pInputText
      type="text"
      placeholder="Rechercher..."
      (input)="dt.filterGlobal($any($event.target).value, 'contains')"
    />
  </span>
</div>

<p-table
  #dt
  [value]="users()"
  [globalFilterFields]="['name', 'email', 'role']"
  [paginator]="true"
  [rows]="10">
  <!-- header / body -->
</p-table>

Filtre par colonne avec p-columnFilter

<p-table [value]="orders()" [paginator]="true" [rows]="10">
  <ng-template pTemplate="header">
    <tr>
      <th>
        Référence
        <p-columnFilter type="text" field="ref" matchMode="contains"></p-columnFilter>
      </th>
      <th>
        Statut
        <p-columnFilter field="status" matchMode="equals" [showMenu]="false">
          <ng-template pTemplate="filter" let-value let-filter="filterCallback">
            <p-dropdown
              [options]="statusOptions"
              [ngModel]="value"
              (onChange)="filter($event.value)"
              placeholder="Tous"
              [showClear]="true">
            </p-dropdown>
          </ng-template>
        </p-columnFilter>
      </th>
      <th>
        Total HT
        <p-columnFilter type="numeric" field="totalHT" currency="EUR"></p-columnFilter>
      </th>
      <th>
        Date
        <p-columnFilter type="date" field="createdAt" matchMode="dateAfter"></p-columnFilter>
      </th>
    </tr>
  </ng-template>
  <!-- body -->
</p-table>

Modes de correspondance disponibles

matchModeDescriptionType cible
containsContient la sous-chaînetext
startsWith / endsWithCommence / finit partext
equals / notEqualsÉgalité strictetous
lt / lte / gt / gteComparaisons numériquesnumeric
dateBefore / dateAfterAvant / après une datedate
betweenEntre deux bornesnumeric / date
inValeur dans une listetous

Persistance des filtres en localStorage

<p-table
  #dt
  [value]="orders()"
  stateStorage="local"
  stateKey="orders-table-state"
  [paginator]="true"
  [rows]="10">
  <!-- ... -->
</p-table>
stateStorage="local" sauvegarde tri, filtres, pagination et largeur de colonnes dans localStorage. stateStorage="session" utilise sessionStorage. Idéal pour reprendre une session de tri complexe sans recoder une persistance.

Pagination et templates

Pagination standard

<p-table
  [value]="orders()"
  [paginator]="true"
  [rows]="10"
  [rowsPerPageOptions]="[10, 25, 50, 100]"
  [showCurrentPageReport]="true"
  currentPageReportTemplate="{first} – {last} sur {totalRecords} commandes">
  <!-- ... -->
</p-table>

Boutons personnalisés autour du paginateur

<p-table [value]="orders()" [paginator]="true" [rows]="10">
  <ng-template pTemplate="paginatorleft">
    <button class="btn btn-sm btn-outline-primary me-2" (click)="refresh()">
      <i class="pi pi-refresh"></i> Recharger
    </button>
  </ng-template>
  <ng-template pTemplate="paginatorright">
    <span class="text-muted">Mise à jour : {{ lastRefresh() | date:'HH:mm:ss' }}</span>
  </ng-template>
  <!-- header / body -->
</p-table>

Message vide personnalisé

<ng-template pTemplate="emptymessage">
  <tr>
    <td colspan="5" class="text-center py-5">
      <i class="pi pi-inbox text-muted" style="font-size:3rem"></i>
      <p class="mt-3 text-muted">Aucune commande ne correspond aux filtres.</p>
      <button class="btn btn-sm btn-outline-secondary" (click)="dt.clear()">
        Réinitialiser les filtres
      </button>
    </td>
  </tr>
</ng-template>

Lazy load — pagination côté serveur

Quand votre dataset dépasse quelques milliers de lignes, charger toutes les données en mémoire devient inefficace. Le mode [lazy]="true" délègue tri, filtres et pagination à votre API backend.

Composant complet en mode lazy

// orders-page.component.ts
import { Component, inject, signal } from '@angular/core';
import { TableModule, TableLazyLoadEvent } from 'primeng/table';
import { OrdersService } from './orders.service';
import { Order } from './order.model';

@Component({
  selector: 'app-orders-page',
  standalone: true,
  imports: [TableModule],
  template: `
    <p-table
      [value]="orders()"
      [lazy]="true"
      [paginator]="true"
      [rows]="20"
      [totalRecords]="total()"
      [loading]="loading()"
      (onLazyLoad)="loadOrders($event)">

      <ng-template pTemplate="header">
        <tr>
          <th pSortableColumn="ref">Réf <p-sortIcon field="ref"/></th>
          <th pSortableColumn="customer">Client <p-sortIcon field="customer"/></th>
          <th pSortableColumn="total">Total <p-sortIcon field="total"/></th>
          <th pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"/></th>
        </tr>
      </ng-template>

      <ng-template pTemplate="body" let-o>
        <tr>
          <td>{{ o.ref }}</td>
          <td>{{ o.customer }}</td>
          <td>{{ o.total | currency:'EUR' }}</td>
          <td>{{ o.createdAt | date:'dd/MM/yyyy' }}</td>
        </tr>
      </ng-template>
    </p-table>
  `,
})
export class OrdersPageComponent {
  private api = inject(OrdersService);

  orders  = signal<Order[]>([]);
  total   = signal<number>(0);
  loading = signal<boolean>(false);

  loadOrders(event: TableLazyLoadEvent): void {
    this.loading.set(true);

    // event contient : first, rows, sortField, sortOrder, filters
    const params = {
      offset:    event.first ?? 0,
      limit:     event.rows  ?? 20,
      sortField: (event.sortField as string) ?? 'createdAt',
      sortOrder: event.sortOrder ?? -1,
      filters:   event.filters,
    };

    this.api.search(params).subscribe(res => {
      this.orders.set(res.items);
      this.total.set(res.totalCount);
      this.loading.set(false);
    });
  }
}

Service backend correspondant

// orders.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface OrderSearchResult {
  items: Order[];
  totalCount: number;
}

@Injectable({ providedIn: 'root' })
export class OrdersService {
  private http = inject(HttpClient);

  search(p: any): Observable<OrderSearchResult> {
    let params = new HttpParams()
      .set('offset',    p.offset)
      .set('limit',     p.limit)
      .set('sortField', p.sortField)
      .set('sortOrder', p.sortOrder);

    // Sérialiser les filtres p-table en query params API
    if (p.filters?.['ref']?.value) {
      params = params.set('ref_contains', p.filters['ref'].value);
    }
    return this.http.get<OrderSearchResult>('/api/orders', { params });
  }
}
Debounce sur lazy load : chaque frappe dans un filtre déclenche onLazyLoad. Pour limiter les appels API, ajoutez filterDelay="300" sur la <p-table> — le délai de 300 ms évite des dizaines de requêtes inutiles.

Virtual scroll : 10 000 lignes fluides

Pour afficher des datasets massifs sans pagination (logs, données de capteurs, time series), le virtual scroll ne rend que les lignes visibles dans le viewport. PrimeNG s'appuie sur le CDK Virtual Scroll d'Angular.

<p-table
  [value]="logs()"
  [scrollable]="true"
  scrollHeight="500px"
  [virtualScroll]="true"
  [virtualScrollItemSize]="38">

  <ng-template pTemplate="header">
    <tr>
      <th style="width:140px">Timestamp</th>
      <th style="width:80px">Niveau</th>
      <th>Message</th>
    </tr>
  </ng-template>

  <ng-template pTemplate="body" let-log>
    <tr style="height:38px">
      <td>{{ log.ts | date:'HH:mm:ss.SSS' }}</td>
      <td>
        <span class="badge" [ngClass]="logBadge(log.level)">
          {{ log.level }}
        </span>
      </td>
      <td>{{ log.message }}</td>
    </tr>
  </ng-template>
</p-table>
Règles du virtual scroll :
  • virtualScrollItemSize doit correspondre exactement à la hauteur en pixels de chaque ligne
  • Définir scrollHeight (px ou flex) — sans hauteur, le virtual scroll ne s'active pas
  • Utiliser une fonction trackBy pour stabiliser les row recycling
  • Éviter les hauteurs variables — désactivez le wrap CSS sur les cellules ou utilisez text-overflow:ellipsis

Sélection de lignes

Sélection multiple avec cases à cocher

// users-selection.component.ts
import { Component, signal } from '@angular/core';
import { TableModule } from 'primeng/table';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-users-selection',
  standalone: true,
  imports: [TableModule, FormsModule],
  template: `
    <div class="mb-3 d-flex gap-2">
      <button class="btn btn-sm btn-outline-danger"
        [disabled]="selected().length === 0"
        (click)="deleteSelected()">
        Supprimer ({{ selected().length }})
      </button>
      <button class="btn btn-sm btn-outline-secondary" (click)="selected.set([])">
        Désélectionner tout
      </button>
    </div>

    <p-table
      [value]="users()"
      [(selection)]="selectedRows"
      dataKey="id"
      [paginator]="true" [rows]="10">

      <ng-template pTemplate="header">
        <tr>
          <th style="width:3rem">
            <p-tableHeaderCheckbox></p-tableHeaderCheckbox>
          </th>
          <th>Nom</th><th>Email</th>
        </tr>
      </ng-template>

      <ng-template pTemplate="body" let-u>
        <tr>
          <td><p-tableCheckbox [value]="u"></p-tableCheckbox></td>
          <td>{{ u.name }}</td>
          <td>{{ u.email }}</td>
        </tr>
      </ng-template>
    </p-table>
  `,
})
export class UsersSelectionComponent {
  users         = signal<User[]>([/* ... */]);
  selectedRows: User[] = [];
  selected      = signal<User[]>([]);

  deleteSelected(): void {
    const ids = this.selectedRows.map(u => u.id);
    this.users.update(list => list.filter(u => !ids.includes(u.id)));
    this.selectedRows = [];
  }
}
Le binding [(selection)]="selectedRows" requiert que selectedRows soit une référence stable (propriété de classe, pas un Signal directement). Pour synchroniser avec un Signal, utilisez (selectionChange)="selected.set($event)".

Export CSV / Excel / PDF

Export CSV natif

<div class="d-flex gap-2 mb-2">
  <button class="btn btn-sm btn-outline-primary" (click)="dt.exportCSV()">
    <i class="pi pi-file"></i> CSV (toutes lignes filtrées)
  </button>
  <button class="btn btn-sm btn-outline-primary"
    (click)="dt.exportCSV({ selectionOnly: true })"
    [disabled]="selectedRows.length === 0">
    <i class="pi pi-check-square"></i> CSV (sélection)
  </button>
</div>

<p-table
  #dt
  [value]="orders()"
  [exportFilename]="'commandes-' + (today | date:'yyyy-MM-dd')">
  <!-- ... -->
</p-table>

Export Excel via SheetJS

// orders-export.component.ts
import { Component, viewChild, signal } from '@angular/core';
import { Table, TableModule } from 'primeng/table';
import * as XLSX from 'xlsx';

@Component({
  selector: 'app-orders-export',
  standalone: true,
  imports: [TableModule],
  template: `
    <button class="btn btn-success btn-sm mb-2" (click)="exportExcel()">
      <i class="pi pi-file-excel"></i> Export Excel
    </button>
    <p-table #dt [value]="orders()"><!-- ... --></p-table>
  `,
})
export class OrdersExportComponent {
  dt      = viewChild.required<Table>('dt');
  orders  = signal<Order[]>([/* ... */]);

  exportExcel(): void {
    // dt.filteredValue contient les données après filtres
    const data = this.dt().filteredValue ?? this.orders();
    const ws   = XLSX.utils.json_to_sheet(data);
    const wb   = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Commandes');
    XLSX.writeFile(wb, `commandes-${new Date().toISOString().slice(0, 10)}.xlsx`);
  }
}

Export PDF avec jsPDF + autoTable

import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';

exportPDF(): void {
  const data = this.dt().filteredValue ?? this.orders();
  const doc  = new jsPDF();

  doc.setFontSize(14);
  doc.text('Liste des commandes', 14, 15);

  autoTable(doc, {
    head: [['Réf', 'Client', 'Total', 'Date']],
    body: data.map(o => [
      o.ref, o.customer, o.total.toFixed(2) + ' €',
      new Date(o.createdAt).toLocaleDateString('fr-FR'),
    ]),
    startY: 25,
    theme: 'striped',
  });

  doc.save(`commandes-${Date.now()}.pdf`);
}

Optimisations et bonnes pratiques

trackBy pour limiter les re-renders

<p-table [value]="users()" [trackBy]="trackById">
  <!-- ... -->
</p-table>
// trackBy : Angular ne ré-instancie que les lignes dont l'id a changé
trackById = (_index: number, item: User) => item.id;

OnPush + Signals = 0 zone.js inutile

import { ChangeDetectionStrategy } from '@angular/core';

@Component({
  // ...
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OrdersPageComponent { /* ... */ }

Charger PrimeNG en lazy loading par route

// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'orders',
    // Le bundle PrimeNG TableModule n'est téléchargé qu'à l'ouverture de /orders
    loadComponent: () => import('./orders/orders-page.component')
      .then(m => m.OrdersPageComponent),
  },
];
Checklist performance p-table :
  • Toujours définir trackBy sur les listes > 100 lignes
  • Activer ChangeDetectionStrategy.OnPush sur le composant parent
  • Mode lazy dès que le dataset dépasse 1000 lignes
  • Mode virtualScroll pour des affichages sans pagination > 5000 lignes
  • Utiliser filterDelay="300" pour debouncer les filtres serveur
  • Charger PrimeNG en lazy loading par route via loadComponent
  • Préférer les Signals pour [value] — déclenche moins de cycles CD

Conclusion

<p-table> couvre 90 % des besoins d'affichage tabulaire en entreprise sans avoir à recoder tri, filtres ou pagination. Du simple tableau de 50 lignes à la grille temps réel de 100 000 entrées, le composant s'adapte via trois leviers : paginator pour les volumes modestes, lazy pour les API paginées, et virtualScroll pour les datasets massifs.

Combiné avec les Signals d'Angular 17+, le mode standalone et OnPush, p-table reste fluide même avec des opérations complexes (filtres multi-critères, tri multi-colonnes, sélection persistée). Pensez à stateStorage pour offrir à vos utilisateurs une expérience qui se souvient de leurs préférences entre deux sessions.

Partager