Table displays data in tabular format.
import { TableModule } from 'primeng/table';
DataTable requires a collection to display along with column components for the representation of the data.
<p-table [value]="products" [tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
Columns can be defined dynamically using the *ngFor directive.
<p-table [columns]="cols" [value]="products" [tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header let-columns>
<tr>
<th *ngFor="let col of columns">
{{ col.header }}
</th>
</tr>
</ng-template>
<ng-template #body let-rowData let-columns="columns">
<tr>
<td *ngFor="let col of columns">
{{ rowData[col.field] }}
</td>
</tr>
</ng-template>
</p-table>
Custom content at header, body and footer sections are supported via templating.
<p-table [value]="products" [tableStyle]="{ 'min-width': '60rem' }">
<ng-template #caption>
<div class="flex items-center justify-between">
<span class="text-xl font-bold">Products</span>
<p-button icon="pi pi-refresh" rounded raised />
</div>
</ng-template>
<ng-template #header>
<tr>
<th>Name</th>
<th>Image</th>
<th>Price</th>
<th>Category</th>
<th>Reviews</th>
<th>Status</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{ product.name }}</td>
<td>
<img
[src]="'https://primefaces.org/cdn/primeng/images/demo/product/' + product.image"
[alt]="product.name"
class="w-24 rounded"
/>
</td>
<td>{{ product.price | currency: 'USD' }}</td>
<td>{{ product.category }}</td>
<td><p-rating [(ngModel)]="product.rating" [readonly]="true" [cancel]="false" /></td>
<td>
<p-tag [value]="product.inventoryStatus" [severity]="getSeverity(product.inventoryStatus)" />
</td>
</tr>
</ng-template>
<ng-template #footer> In total there are {{ products ? products.length : 0 }} products. </ng-template>
</p-table>
In addition to a regular table, alternatives with alternative sizes are available.
<div class="flex justify-center mb-4">
<p-selectbutton
[options]="sizes"
[(ngModel)]="selectedSize"
[multiple]="false"
optionLabel="name"
optionValue="class" />
</div>
<p-table [value]="products" [tableStyle]="{ 'min-width': '50rem' }" [styleClass]="selectedSize.class">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
Enabling showGridlines displays borders between cells.
<p-table
[value]="products"
showGridlines
[tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
Alternating rows are displayed when stripedRows property is present.
<p-table
[value]="products"
stripedRows
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{product.code}}</td>
<td>{{product.name}}</td>
<td>{{product.category}}</td>
<td>{{product.quantity}}</td>
</tr>
</ng-template>
</p-table>
Certain rows or cells can easily be styled based on conditions.
<p-table [value]="products" [tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr [ngClass]="rowClass(product)" [ngStyle]="rowStyle(product)">
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>
<p-badge [value]="product.quantity" [severity]="stockSeverity(product)" />
</td>
</tr>
</ng-template>
</p-table>
Pagination is enabled by setting paginator property to true and defining a rows property to specify the number of rows per page.
<p-table
[value]="customers"
[paginator]="true"
[rows]="5"
[tableStyle]="{ 'min-width': '50rem' }"
[rowsPerPageOptions]="[5, 10, 20]"
>
<ng-template #header>
<tr>
<th style="width:25%">Name</th>
<th style="width:25%">Country</th>
<th style="width:25%">Company</th>
<th style="width:25%">Representative</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td>{{ customer.name }}</td>
<td>{{ customer.country.name }}</td>
<td>{{ customer.company }}</td>
<td>{{ customer.representative.name }}</td>
</tr>
</ng-template>
</p-table>
Paginator can also be controlled via model using a binding to the first property where changes trigger a pagination.
<div class="mb-4">
<p-button type="button" icon="pi pi-chevron-left" (click)="prev()" [disabled]="isFirstPage()" text />
<p-button type="button" icon="pi pi-refresh" (click)="reset()" text />
<p-button type="button" icon="pi pi-chevron-right" (click)="next()" [disabled]="isLastPage()" text />
</div>
<p-table
[value]="customers"
[paginator]="true"
[rows]="5"
[first]="first"
[showCurrentPageReport]="true"
[tableStyle]="{ 'min-width': '50rem' }"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
(onPage)="pageChange($event)"
[rowsPerPageOptions]="[10, 25, 50]"
>
<ng-template #header>
<tr>
<th style="width:25%">Name</th>
<th style="width:25%">Country</th>
<th style="width:25%">Company</th>
<th style="width:25%">Representative</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td>{{ customer.name }}</td>
<td>{{ customer.country.name }}</td>
<td>{{ customer.company }}</td>
<td>{{ customer.representative.name }}</td>
</tr>
</ng-template>
<ng-template #paginatorleft>
<p-button type="button" icon="pi pi-plus" text />
</ng-template>
<ng-template #paginatorright>
<p-button type="button" icon="pi pi-cloud" text />
</ng-template>
</p-table>
A column can be made sortable by adding the pSortableColumn directive whose value is the field to sort against and a sort indicator via p-sortIcon component. For dynamic columns, setting pSortableColumnDisabled property as true disables sorting for that particular column.
Default sorting is executed on a single column, in order to enable multiple field sorting, set sortMode property to "multiple" and use metakey when clicking on another column.
<p-table [value]="products" [tableStyle]="{'min-width': '60rem'}">
<ng-template #header>
<tr>
<th pSortableColumn="code" style="width:20%">
Code <p-sortIcon field="code" />
</th>
<th pSortableColumn="name" style="width:20%">
Name <p-sortIcon field="name" />
</th>
<th pSortableColumn="category" style="width:20%">
Category <p-sortIcon field="category" />
</th>
<th pSortableColumn="quantity" style="width:20%">
Quantity <p-sortIcon field="quantity" />
</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
Multiple columns can be sorted by defining sortMode as multiple. This mode requires metaKey (e.g. ⌘) to be pressed when clicking a header.
<p-table [value]="products1" [tableStyle]="{'min-width': '60rem'}">
<ng-template #header>
<tr>
<th pSortableColumn="code" style="width:20%">
Code <p-sortIcon field="code" />
</th>
<th pSortableColumn="name" style="width:20%">
Name <p-sortIcon field="name" />
</th>
<th pSortableColumn="category" style="width:20%">
Category <p-sortIcon field="category" />
</th>
<th pSortableColumn="quantity" style="width:20%">
Quantity <p-sortIcon field="quantity" />
</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
Defining a default sortField and sortOrder displays data as sorted initially in single column sorting. In multiple sort mode, multiSortMeta should be used instead by providing an array of DataTableSortMeta objects.
<p-table [value]="products" sortField="price" [sortOrder]="-1" [tableStyle]="{ 'min-width': '60rem' }">
<ng-template #header>
<tr>
<th pSortableColumn="code" style="width:20%">
Code <p-sortIcon field="code" />
</th>
<th pSortableColumn="name" style="width:20%">
Name <p-sortIcon field="name" />
</th>
<th pSortableColumn="price" style="width:20%">
Price <p-sortIcon field="price" />
</th>
<th pSortableColumn="category" style="width:20%">
Category <p-sortIcon field="category" />
</th>
<th pSortableColumn="quantity" style="width:20%">
Quantity <p-sortIcon field="quantity" />
</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.price | currency : 'USD' }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
The removable sort can be implemented using the customSort property.
<p-table #dt [value]="products" (sortFunction)="customSort($event)" [customSort]="true">
<ng-template #header>
<tr>
<th pSortableColumn="code">
Code <p-sortIcon field="code" />
</th>
<th pSortableColumn="name">
Name <p-sortIcon field="name" />
</th>
<th pSortableColumn="category">
Category <p-sortIcon field="category" />
</th>
<th pSortableColumn="quantity">
Quantity <p-sortIcon field="quantity" />
</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
Data filtering is enabled by defining the filters property referring to a DataTableFilterMeta instance. Each column to filter also requires filter to be enabled. Built-in filter element is a input field and using filterElement, it is possible to customize the filtering with your own UI.
The optional global filtering searches the data against a single value that is bound to the global key of the filters object. The fields to search against is defined with the globalFilterFields.
<p-table
#dt2
[value]="customers"
dataKey="id"
[rows]="10"
[rowsPerPageOptions]="[10, 25, 50]"
[loading]="loading"
[paginator]="true"
[globalFilterFields]="['name', 'country.name', 'representative.name', 'status']"
[tableStyle]="{ 'min-width': '75rem' }"
>
<ng-template #caption>
<div class="flex">
<p-iconfield iconPosition="left" class="ml-auto">
<p-inputicon>
<i class="pi pi-search"></i>
</p-inputicon>
<input
pInputText
type="text"
(input)="dt2.filterGlobal($event.target.value, 'contains')"
placeholder="Search keyword"
/>
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width:22%">Name</th>
<th style="width:22%">Country</th>
<th style="width:22%">Agent</th>
<th style="width:22%">Status</th>
<th style="width:12%">Verified</th>
</tr>
<tr>
<th>
<p-columnFilter
type="text"
field="name"
placeholder="Search by name"
ariaLabel="Filter Name"
></p-columnFilter>
</th>
<th>
<p-columnFilter
type="text"
field="country.name"
placeholder="Search by country"
ariaLabel="Filter Country"
></p-columnFilter>
</th>
<th>
<p-columnFilter field="representative" matchMode="in" [showMenu]="false">
<ng-template #filter let-value let-filter="filterCallback">
<p-multiselect
[(ngModel)]="value"
[options]="representatives"
placeholder="Any"
(onChange)="filter($event.value)"
optionLabel="name"
style="min-width: 14rem"
>
<ng-template let-option #item>
<div class="inline-block align-middle">
<img
[alt]="option.label"
src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ option.image }}"
width="24"
class="align-middle"
/>
<span class="ml-1 mt-1">{{ option.name }}</span>
</div>
</ng-template>
</p-multiselect>
</ng-template>
</p-columnFilter>
</th>
<th>
<p-columnFilter field="status" matchMode="equals" [showMenu]="false">
<ng-template #filter let-value let-filter="filterCallback">
<p-select
[(ngModel)]="value"
[options]="statuses"
(onChange)="filter($event.value)"
placeholder="Select One"
[showClear]="true"
style="min-width: 12rem"
>
<ng-template let-option #item>
<p-tag [value]="option.value" [severity]="getSeverity(option.value)" />
</ng-template>
</p-select>
</ng-template>
</p-columnFilter>
</th>
<th>
<p-columnFilter type="boolean" field="verified"></p-columnFilter>
</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td>
{{ customer.name }}
</td>
<td>
<div class="flex items-center gap-2">
<img src="https://primefaces.org/cdn/primeng/images/demo/flag/flag_placeholder.png" [class]="'flag flag-' + customer.country.code" style="width: 20px" />
<span>{{ customer.country.name }}</span>
</div>
</td>
<td>
<div class="flex items-center gap-2">
<img [alt]="customer.representative.name" src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ customer.representative.image }}" width="32" style="vertical-align: middle" />
<span>{{ customer.representative.name }}</span>
</div>
</td>
<td>
<p-tag [value]="customer.status" [severity]="getSeverity(customer.status)" />
</td>
<td>
<i
class="pi"
[ngClass]="{
'text-green-500 pi-check-circle': customer.verified,
'text-red-500 pi-times-circle': !customer.verified
}"
></i>
</td>
</tr>
</ng-template>
<ng-template #emptymessage>
<tr>
<td colspan="5">No customers found.</td>
</tr>
</ng-template>
</p-table>
Filters are displayed in an overlay.
<p-table
#dt1
[value]="customers"
dataKey="id"
[rows]="10"
[rowsPerPageOptions]="[10, 25, 50]"
[loading]="loading"
[paginator]="true"
[globalFilterFields]="['name', 'country.name', 'representative.name', 'status']"
>
<ng-template #caption>
<div class="flex">
<p-button label="Clear" [outlined]="true" icon="pi pi-filter-slash" (click)="clear(dt1)" />
<p-iconfield iconPosition="left" class="ml-auto">
<p-inputicon>
<i class="pi pi-search"></i>
</p-inputicon>
<input pInputText type="text" (input)="dt2.filterGlobal($event.target.value, 'contains')" placeholder="Search keyword" />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="min-width:15rem">
<div class="flex items-center">
Name
<p-columnFilter type="text" field="name" display="menu" />
</div>
</th>
<th style="min-width:15rem">
<div class="flex items-center">
Country
<p-columnFilter type="text" field="country.name" display="menu" />
</div>
</th>
<th style="min-width:15rem">
<div class="flex items-center">
Agent
<p-columnFilter field="representative" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showAddButton]="false">
<ng-template #header>
<div class="px-4 pt-4 pb-0">
<span class="font-bold">Agent Picker</span>
</div>
</ng-template>
<ng-template #filter let-value let-filter="filterCallback">
<p-multiSelect [(ngModel)]="value" [options]="representatives" placeholder="Any" (onChange)="filter($event.value)" optionLabel="name">
<ng-template let-option #item>
<div class="inline-block align-middle">
<img [alt]="option.label" src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ option.image }}" width="24" class="align-middle" />
<span class="ml-1 mt-1">{{ option.name }}</span>
</div>
</ng-template>
</p-multiSelect>
</ng-template>
</p-columnFilter>
</div>
</th>
<th style="min-width:10rem">
<div class="flex items-center">
Date
<p-columnFilter type="date" field="date" display="menu" />
</div>
</th>
<th style="min-width:10rem">
<div class="flex items-center">
Balance
<p-columnFilter type="numeric" field="balance" display="menu" currency="USD" />
</div>
</th>
<th style="min-width:10rem">
<div class="flex items-center">
Status
<p-columnFilter field="status" matchMode="equals" display="menu">
<ng-template #filter let-value let-filter="filterCallback">
<p-dropdown [(ngModel)]="value" [options]="statuses" (onChange)="filter($event.value)" placeholder="Any">
<ng-template let-option #item>
<p-tag [value]="option.value" [severity]="getSeverity(option.label)" />
</ng-template>
</p-dropdown>
</ng-template>
</p-columnFilter>
</div>
</th>
<th style="min-width:10rem">
<div class="flex items-center">
Activity
<p-columnFilter field="activity" matchMode="between" display="menu" [showMatchModes]="false" [showOperator]="false" [showAddButton]="false">
<ng-template #filter let-filter="filterCallback">
<p-slider [(ngModel)]="activityValues" [range]="true" (onSlideEnd)="filter($event.values)" styleClass="m-4" />
<div class="flex items-center px-2">
<span>{{ activityValues[0] }}</span>
<span>{{ activityValues[1] }}</span>
</div>
</ng-template>
</p-columnFilter>
</div>
</th>
<th style="width: 3rem">
<div class="flex items-center">
Verified
<p-columnFilter type="boolean" field="verified" display="menu" />
</div>
</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td>
{{ customer.name }}
</td>
<td>
<div class="flex items-center gap-2">
<img src="https://primefaces.org/cdn/primeng/images/demo/flag/flag_placeholder.png" [class]="'flag flag-' + customer.country.code" style="width: 20px" />
<span >{{ customer.country.name }}</span>
</div>
</td>
<td>
<div class="flex items-center gap-2">
<img [alt]="customer.representative.name" src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ customer.representative.image }}" width="32" />
<span >{{ customer.representative.name }}</span>
</div>
</td>
<td>
{{ customer.date | date: 'MM/dd/yyyy' }}
</td>
<td>
{{ customer.balance | currency: 'USD' : 'symbol' }}
</td>
<td>
<p-tag [value]="customer.status" [severity]="getSeverity(customer.status)" />
</td>
<td>
<p-progressbar [value]="customer.activity" [showValue]="false" />
</td>
<td class="text-center">
<i
class="pi"
[ngClass]="{
'text-green-500 pi-check-circle': customer.verified,
'text-red-500 pi-times-circle': !customer.verified
}"
></i>
</td>
</tr>
</ng-template>
<ng-template #emptymessage>
<tr>
<td colspan="7">No customers found.</td>
</tr>
</ng-template>
</p-table>
Single row selection is enabled by defining selectionMode as single along with a value binding using selection property. When available, it is suggested to provide a unique identifier of a row with dataKey to optimize performance.
By default, metaKey press (e.g. ⌘) is necessary to unselect a row however this can be configured with disabling the metaKeySelection property. In touch enabled devices this option has no effect and behavior is same as setting it to false.
<p-toggleswitch [(ngModel)]="metaKey" inputId="input-metakey" />
<p-table
[value]="products"
selectionMode="single"
[(selection)]="selectedProduct"
[metaKeySelection]="metaKey" dataKey="id"
[tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr [pSelectableRow]="product">
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
More than one row is selectable by setting selectionMode to multiple. By default in multiple selection mode, metaKey press (e.g. ⌘) is not necessary to add to existing selections. When the optional metaKeySelection is present, behavior is changed in a way that selecting a new row requires meta key to be present. Note that in touch enabled devices, DataTable always ignores metaKey.
<div class="flex justify-center items-center mb-6 gap-2">
<p-toggleswitch [(ngModel)]="metaKey" inputId="input-metakey" />
<label for="input-metakey">MetaKey</label>
</div>
<p-table
[value]="products"
selectionMode="multiple"
[(selection)]="selectedProducts"
[metaKeySelection]="metaKey"
dataKey="code"
[tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product let-rowIndex="rowIndex">
<tr [pSelectableRow]="product" [pSelectableRowIndex]="rowIndex">
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
</tr>
</ng-template>
</p-table>
Single selection can also be handled using radio buttons.
<p-table
[value]="products"
[(selection)]="selectedProduct"
dataKey="code"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th style="width: 4rem"></th>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>
<p-tableRadioButton [value]="product" />
</td>
<td>{{product.code}}</td>
<td>{{product.name}}</td>
<td>{{product.category}}</td>
<td>{{product.quantity}}</td>
</tr>
</ng-template>
</p-table>
Multiple selection can also be handled using checkboxes by enabling the selectionMode property of column as multiple.
<p-table
[value]="products"
[(selection)]="selectedProducts"
dataKey="code"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th style="width: 4rem"><p-tableHeaderCheckbox /></th>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>
<p-tableCheckbox [value]="product" />
</td>
<td>{{product.code}}</td>
<td>{{product.name}}</td>
<td>{{product.category}}</td>
<td>{{product.quantity}}</td>
</tr>
</ng-template>
</p-table>
Row selection with an element inside a column is implemented with templating.
<p-toast />
<p-table [value]="products" [tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
<th style="width: 5rem"></th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{ product.code }}</td>
<td>{{ product.name }}</td>
<td>{{ product.category }}</td>
<td>{{ product.quantity }}</td>
<td>
<p-button icon="pi pi-search" (click)="selectProduct(product)" severity="secondary" rounded />
</td>
</tr>
</ng-template>
</p-table>
Table provides onRowSelect and onRowUnselect events to listen selection events.
<p-toast />
<p-table
[value]="products"
selectionMode="single"
[(selection)]="selectedProduct"
dataKey="code"
(onRowSelect)="onRowSelect($event)"
(onRowUnselect)="onRowUnselect($event)"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr [pSelectableRow]="product">
<td>{{product.code}}</td>
<td>{{product.name}}</td>
<td>{{product.category}}</td>
<td>{{product.quantity}}</td>
</tr>
</ng-template>
</p-table>
Row expansion allows displaying detailed content for a particular row. To use this feature, add a template named rowexpansion and use the pRowToggler directive whose value is the row data instance on an element of your choice whose click event toggles the expansion. This enables providing your custom UI such as buttons, links and so on. Example below uses an anchor with an icon as a toggler. Setting pRowTogglerDisabled as true disables the toggle event for the element.
<p-toast />
<p-table [value]="products" dataKey="id" [tableStyle]="{ 'min-width': '60rem' }" [expandedRowKeys]="expandedRows" (onRowExpand)="onRowExpand($event)" (onRowCollapse)="onRowCollapse($event)">
<ng-template #caption>
<div class="flex flex-wrap justify-end gap-2">
<p-button label="Expand All" icon="pi pi-plus" text (onClick)="expandAll()" />
<p-button label="Collapse All" icon="pi pi-minus" text (onClick)="collapseAll()" />
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 5rem"></th>
<th pSortableColumn="name">Name <p-sortIcon field="name" /></th>
<th>Image</th>
<th pSortableColumn="price">Price <p-sortIcon field="price" /></th>
<th pSortableColumn="category">Category <p-sortIcon field="category" /></th>
<th pSortableColumn="rating">Reviews <p-sortIcon field="rating" /></th>
<th pSortableColumn="inventoryStatus">Status <p-sortIcon field="inventoryStatus" /></th>
</tr>
</ng-template>
<ng-template #body let-product let-expanded="expanded">
<tr>
<td>
<p-button type="button" pRipple [pRowToggler]="product" [text]="true" [rounded]="true" [plain]="true" [icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'" />
</td>
<td>{{ product.name }}</td>
<td><img [src]="'https://primefaces.org/cdn/primeng/images/demo/product/' + product.image" [alt]="product.name" width="50" class="shadow-lg" /></td>
<td>{{ product.price | currency : 'USD' }}</td>
<td>{{ product.category }}</td>
<td><p-rating [ngModel]="product.rating" [readonly]="true" [cancel]="false" /></td>
<td>
<p-tag [value]="product.inventoryStatus" [severity]="getSeverity(product.inventoryStatus)" />
</td>
</tr>
</ng-template>
<ng-template #expandedrow let-product>
<tr>
<td colspan="7">
<div class="p-4">
<h5>Orders for {{ product.name }}</h5>
<p-table [value]="product.orders" dataKey="id">
<ng-template #header>
<tr>
<th pSortableColumn="id">Id <p-sortIcon field="price" /></th>
<th pSortableColumn="customer">Customer <p-sortIcon field="customer" /></th>
<th pSortableColumn="date">Date <p-sortIcon field="date" /></th>
<th pSortableColumn="amount">Amount <p-sortIcon field="amount" /></th>
<th pSortableColumn="status">Status <p-sortIcon field="status" /></th>
<th style="width: 4rem"></th>
</tr>
</ng-template>
<ng-template #body let-order>
<tr>
<td>{{ order.id }}</td>
<td>{{ order.customer }}</td>
<td>{{ order.date }}</td>
<td>{{ order.amount | currency : 'USD' }}</td>
<td>
<p-tag [value]="order.status" [severity]="getStatusSeverity(order.status)" />
</td>
<td><p-button type="button" icon="pi pi-search" /></td>
</tr>
</ng-template>
<ng-template #emptymessage>
<tr>
<td colspan="6">There are no order for this product yet.</td>
</tr>
</ng-template>
</p-table>
</div>
</td>
</tr>
</ng-template>
</p-table>
In-cell editing is enabled by adding pEditableColumn directive to an editable cell that has a p-cellEditor helper component to define the input-output templates for the edit and view modes respectively.
<p-table
[value]="products" dataKey="id"
[tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header>
<tr>
<th style="width:25%">
Code
</th>
<th style="width:25%">
Name
</th>
<th style="width:25%">
Inventory Status
</th>
<th style="width:25%">
Price
</th>
</tr>
</ng-template>
<ng-template #body let-product let-editing="editing">
<tr>
<td [pEditableColumn]="product.code" pEditableColumnField="code">
<p-cellEditor>
<ng-template #input>
<input
pInputText
type="text"
[(ngModel)]="product.code" />
</ng-template>
<ng-template #output>
{{ product.code }}
</ng-template>
</p-cellEditor>
</td>
<td [pEditableColumn]="product.name" pEditableColumnField="name">
<p-cellEditor>
<ng-template #input>
<input
pInputText
type="text"
[(ngModel)]="product.name"
required />
</ng-template>
<ng-template #output>
{{ product.name }}
</ng-template>
</p-cellEditor>
</td>
<td [pEditableColumn]="product.inventoryStatus" pEditableColumnField="inventoryStatus">
<p-cellEditor>
<ng-template #input>
<input
pInputText
[(ngModel)]="product.inventoryStatus" />
</ng-template>
<ng-template #output>
{{ product.inventoryStatus }}
</ng-template>
</p-cellEditor>
</td>
<td [pEditableColumn]="product.price" pEditableColumnField="price">
<p-cellEditor>
<ng-template #input>
<input
pInputText type="text"
[(ngModel)]="product.price" />
</ng-template>
<ng-template #output>
{{ product.price | currency: 'USD' }}
</ng-template>
</p-cellEditor>
</td>
</tr>
</ng-template>
</p-table>
Row editing toggles the visibility of all the editors in the row at once and provides additional options to save and cancel editing. Row editing functionality is enabled by setting the editMode to "row" on table, defining a dataKey to uniquely identify a row, adding pEditableRow directive to the editable rows and defining the UI Controls with pInitEditableRow, pSaveEditableRow and pCancelEditableRow directives respectively.
Save and Cancel functionality implementation is left to the page author to provide more control over the editing business logic. Example below utilizes a simple implementation where a row is cloned when editing is initialized and is saved or restored depending on the result of the editing. An implicit variable called "editing" is passed to the body template so you may come up with your own UI controls that implement editing based on your own requirements such as adding validations and styling. Note that pSaveEditableRow only switches the row to back view mode when there are no validation errors.
Moreover, you may use setting pEditableRowDisabled property as true to disable editing for that particular row and in case you need to display rows in edit mode by default, use the editingRowKeys property which is a map whose key is the dataKey of the record where the value is any arbitrary number greater than zero.
<p-table [value]="products" dataKey="id" editMode="row" [tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th style="width:20%">Code</th>
<th style="width:20%">Name</th>
<th style="width:20%">Inventory Status</th>
<th style="width:20%">Price</th>
<th style="width:20%"></th>
</tr>
</ng-template>
<ng-template #body let-product let-editing="editing" let-ri="rowIndex">
<tr [pEditableRow]="product">
<td>
<p-cellEditor>
<ng-template #input>
<input
pInputText
type="text"
[(ngModel)]="product.code" />
</ng-template>
<ng-template #output>
{{product.code}}
</ng-template>
</p-cellEditor>
</td>
<td>
<p-cellEditor>
<ng-template #input>
<input
pInputText type="text"
[(ngModel)]="product.name"
required />
</ng-template>
<ng-template #output>
{{product.name}}
</ng-template>
</p-cellEditor>
</td>
<td>
<p-cellEditor>
<ng-template #input>
<p-select
[options]="statuses"
appendTo="body"
[(ngModel)]="product.inventoryStatus"
[style]="{'width':'100%'}" />
</ng-template>
<ng-template #output>
<p-tag
[value]="product.inventoryStatus"
[severity]="getSeverity(product.inventoryStatus)" />
</ng-template>
</p-cellEditor>
</td>
<td>
<p-cellEditor>
<ng-template #input>
<input
pInputText
type="text"
[(ngModel)]="product.price" />
</ng-template>
<ng-template #output>
{{product.price | currency: 'USD'}}
</ng-template>
</p-cellEditor>
</td>
<td>
<div class="flex items-center justify-center gap-2">
<button
*ngIf="!editing"
pButton
pRipple
type="button"
pInitEditableRow
icon="pi pi-pencil"
(click)="onRowEditInit(product)"
text
rounded
severity="secondary"
></button>
<button
*ngIf="editing"
pButton
pRipple
type="button"
pSaveEditableRow
icon="pi pi-check"
(click)="onRowEditSave(product)"
text
rounded
severity="secondary"
></button>
<button
*ngIf="editing"
pButton
pRipple
type="button"
pCancelEditableRow
icon="pi pi-times"
(click)="onRowEditCancel(product, ri)"
text
rounded
severity="secondary"
></button>
</div>
</td>
</tr>
</ng-template>
</p-table>
Adding scrollable property along with a scrollHeight for the data viewport enables vertical scrolling with fixed headers.
<p-table
[value]="customers"
[scrollable]="true"
scrollHeight="400px"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th>Name</th>
<th>Country</th>
<th>Company</th>
<th>Representative</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td>{{customer.name}}</td>
<td>{{customer.country.name}}</td>
<td>{{customer.company}}</td>
<td>{{customer.representative.name}}</td>
</tr>
</ng-template>
</p-table>
Flex scroll feature makes the scrollable viewport section dynamic instead of a fixed value so that it can grow or shrink relative to the parent size of the table. Click the button below to display a maximizable Dialog where data viewport adjusts itself according to the size changes.
<div class="flex justify-center">
<button type="button" (click)="showDialog()" pButton icon="pi pi-external-link" label="Show"></button>
</div>
<p-dialog
header="Header"
[resizable]="false"
[modal]="true"
[maximizable]="true"
appendTo="body"
[(visible)]="dialogVisible"
[style]="{ width: '75vw' }"
[contentStyle]="{ height: '300px' }"
>
<p-table [value]="customers" [scrollable]="true" scrollHeight="flex" [tableStyle]="{ 'min-width': '50rem' }">
<ng-template #header>
<tr>
<th>Name</th>
<th>Country</th>
<th>Company</th>
<th>Representative</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td>{{ customer.name }}</td>
<td>{{ customer.country.name }}</td>
<td>{{ customer.company }}</td>
<td>{{ customer.representative.name }}</td>
</tr>
</ng-template>
</p-table>
<ng-template #footer>
<p-button label="Ok" icon="pi pi-check" (onClick)="dialogVisible = false" />
</ng-template>
</p-dialog>
Horizontal scrollbar is displayed when table width exceeds the parent width.
<p-table [value]="customers" [scrollable]="true" scrollHeight="400px">
<ng-template #header>
<tr>
<th style="min-width:100px">Id</th>
<th style="min-width:200px">Name</th>
<th style="min-width:200px">Country</th>
<th style="min-width:200px">Date</th>
<th style="min-width:200px">Balance</th>
<th style="min-width:200px">Company</th>
<th style="min-width:200px">Status</th>
<th style="min-width:200px">Activity</th>
<th style="min-width:200px">Representative</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td>{{customer.id}}</td>
<td>{{customer.name}}</td>
<td>{{customer.country.name}}</td>
<td>{{customer.date}}</td>
<td>{{formatCurrency(customer.balance)}}</td>
<td>{{customer.company}}</td>
<td>{{customer.status}}</td>
<td>{{customer.activity}}</td>
<td>{{customer.representative.name}}</td>
</tr>
</ng-template>
<ng-template #footer>
<tr>
<td>Id</td>
<td>Name</td>
<td>Country</td>
<td>Date</td>
<td>Balance</td>
<td>Company</td>
<td>Status</td>
<td>Activity</td>
<td>Representative</td>
</tr>
</ng-template>
</p-table>
Frozen rows are used to fix certain rows while scrolling, this data is defined with the frozenValue property.
<p-table
[value]="unlockedCustomers"
[frozenValue]="lockedCustomers"
[scrollable]="true"
scrollHeight="400px"
[tableStyle]="{'min-width': '60rem'}">
<ng-template #header>
<tr>
<th>Name</th>
<th>Country</th>
<th>Company</th>
<th>Representative</th>
<th style="width:5rem"></th>
</tr>
</ng-template>
<ng-template #frozenbody let-customer let-index="rowIndex">
<tr class="font-bold">
<td>{{customer.name}}</td>
<td>{{customer.country.name}}</td>
<td>{{customer.company}}</td>
<td>{{customer.representative.name}}</td>
<td>
<button
pButton
pRipple
type="button"
[icon]="'pi pi-lock-open'"
(click)="toggleLock(customer,true,index)"
size="small"
text>
</button>
</td>
</tr>
</ng-template>
<ng-template #body let-customer let-index="rowIndex">
<tr>
<td>{{customer.name}}</td>
<td>{{customer.country.name}}</td>
<td>{{customer.company}}</td>
<td>{{customer.representative.name}}</td>
<td>
<button
pButton
pRipple
type="button"
[icon]="'pi pi-lock'"
[disabled]="lockedCustomers.length >= 2"
(click)="toggleLock(customer,false,index)"
size="small"
text>
</button>
</td>
</tr>
</ng-template>
</p-table>
Certain columns can be frozen by using the pFrozenColumn directive of the table component. In addition, alignFrozen is available to define whether the column should be fixed on the left or right.
<p-togglebutton
[(ngModel)]="balanceFrozen"
[onIcon]="'pi pi-lock'"
offIcon="pi pi-lock-open"
[onLabel]="'Balance'"
offLabel="Balance" />
<p-table [value]="customers" [scrollable]="true" scrollHeight="400px" styleClass="mt-4">
<ng-template #header>
<tr>
<th style="min-width:200px" pFrozenColumn>Name</th>
<th style="min-width:100px">Id</th>
<th style="min-width:200px">Country</th>
<th style="min-width:200px">Date</th>
<th style="min-width:200px">Company</th>
<th style="min-width:200px">Status</th>
<th style="min-width:200px">Activity</th>
<th style="min-width:200px">Representative</th>
<th style="min-width:200px" alignFrozen="right" pFrozenColumn [frozen]="balanceFrozen">Balance</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td pFrozenColumn>{{customer.name}}</td>
<td style="min-width:100px">{{customer.id}}</td>
<td>{{customer.country.name}}</td>
<td>{{customer.date}}</td>
<td>{{customer.company}}</td>
<td>{{customer.status}}</td>
<td>{{customer.activity}}</td>
<td >{{customer.representative.name}}</td>
<td alignFrozen="right" pFrozenColumn [frozen]="balanceFrozen">{{formatCurrency(customer.balance)}}</td>
</tr>
</ng-template>
</p-table>
VirtualScroller is a performance-approach to handle huge data efficiently. Setting virtualScroll property as true and providing a virtualScrollItemSize in pixels would be enough to enable this functionality. It is also suggested to use the same virtualScrollItemSize value on the tr element inside the body template.
<p-table
[columns]="cols"
[value]="cars"
[scrollable]="true"
scrollHeight="400px"
[virtualScroll]="true"
[virtualScrollItemSize]="46">
<ng-template #header let-columns>
<tr>
<th *ngFor="let col of columns" style="width: 20%;">
{{ col.header }}
</th>
</tr>
</ng-template>
<ng-template #body let-rowData let-rowIndex="rowIndex" let-columns="columns">
<tr style="height:46px">
<td *ngFor="let col of columns">
{{ rowData[col.field] }}
</td>
</tr>
</ng-template>
</p-table>
VirtualScroller is a performance-approach to handle huge data efficiently. Setting virtualScroll property as true and providing a virtualScrollItemSize in pixels would be enough to enable this functionality. It is also suggested to use the same virtualScrollItemSize value on the tr element inside the body template.
<p-table
[columns]="cols"
[value]="virtualCars"
[scrollable]="true"
scrollHeight="400px"
[rows]="100"
[virtualScroll]="true"
[virtualScrollItemSize]="46"
[lazy]="true"
(onLazyLoad)="loadCarsLazy($event)">
<ng-template #header let-columns>
<tr>
<th *ngFor="let col of columns" style="width: 20%;">
{{col.header}}
</th>
</tr>
</ng-template>
<ng-template #body let-rowData let-columns="columns">
<tr style="height:46px">
<td *ngFor="let col of columns">
{{rowData[col.field]}}
</td>
</tr>
</ng-template>
<ng-template #loadingbody let-columns="columns">
<tr style="height:46px">
<td *ngFor="let col of columns; let even = even">
<p-skeleton [ngStyle]="{'width': even ? (col.field === 'year' ? '30%' : '40%') : '60%'}" />
</td>
</tr>
</ng-template>
</p-table>
Columns can be grouped using rowspan and colspan properties.
<p-table [value]="sales" [tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th rowspan="3">Product</th>
<th colspan="4">Sale Rate</th>
</tr>
<tr>
<th colspan="2">Sales</th>
<th colspan="2">Profits</th>
</tr>
<tr>
<th>Last Year</th>
<th>This Year</th>
<th>Last Year</th>
<th>This Year</th>
</tr>
</ng-template>
<ng-template #body let-sale>
<tr>
<td>{{sale.product}}</td>
<td>{{sale.lastYearSale}}%</td>
<td>{{sale.thisYearSale}}%</td>
<td>{{sale.lastYearProfit | currency: 'USD'}}</td>
<td>{{sale.thisYearProfit | currency: 'USD'}}</td>
</tr>
</ng-template>
<ng-template #footer>
<tr>
<td colspan="3" class="text-right font-bold p-3 pb-0">Totals</td>
<td class="font-bold p-3 pb-0">{{ lastYearTotal | currency: 'USD' }}</td>
<td class="font-bold p-3 pb-0">{{ thisYearTotal | currency: 'USD' }}</td>
</tr>
</ng-template>
</p-table>
Rows are grouped with the groupRowsBy property. When rowGroupMode is set as subheader, a header and footer can be displayed for each group. The content of a group header is provided with groupheader and footer with groupfooter templates.
<p-table
[value]="customers"
sortField="representative.name"
sortMode="single"
[scrollable]="true"
scrollHeight="400px"
rowGroupMode="subheader"
groupRowsBy="representative.name"
[tableStyle]="{'min-width': '60rem'}">
<ng-template #header>
<tr>
<th>Name</th>
<th>Country</th>
<th>Company</th>
<th>Status</th>
<th>Date</th>
</tr>
</ng-template>
<ng-template #groupheader let-customer>
<tr pRowGroupHeader>
<td colspan="5">
<div class="flex items-center gap-2">
<img [alt]="customer.representative.name" src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ customer.representative.image }}" width="32" style="vertical-align: middle" />
<span class="font-bold">{{ customer.representative.name }}</span>
</div>
</td>
</tr>
</ng-template>
<ng-template #groupfooter let-customer>
<tr>
<td colspan="5" class="text-right font-bold pr-12">
Total Customers: {{calculateCustomerTotal(customer.representative.name)}}
</td>
</tr>
</ng-template>
<ng-template #body let-customer let-rowIndex="rowIndex">
<tr>
<td>
{{customer.name}}
</td>
<td>
<div class="flex items-center gap-2">
<img src="https://primefaces.org/cdn/primeng/images/demo/flag/flag_placeholder.png" [class]="'flag flag-' + customer.country.code" style="width: 20px" />
<span>{{ customer.country.name }}</span>
</div>
</td>
<td>
{{customer.company}}
</td>
<td>
<p-tag [value]="customer.status" [severity]="getSeverity(customer.status)" />
</td>
<td>
{{customer.date}}
</td>
</tr>
</ng-template>
</p-table>
When expandableRowGroups is present in subheader based row grouping, groups can be expanded and collapsed. State of the expansions are controlled using the expandedRows and onRowToggle properties.
<p-table
[value]="customers"
sortField="representative.name"
sortMode="single"
dataKey="representative.name"
rowGroupMode="subheader"
groupRowsBy="representative.name"
[tableStyle]="{'min-width': '70rem'}">
<ng-template #header>
<tr>
<th style="width:20%">Name</th>
<th style="width:20%">Country</th>
<th style="width:20%">Company</th>
<th style="width:20%">Status</th>
<th style="width:20%">Date</th>
</tr>
</ng-template>
<ng-template #groupheader let-customer let-rowIndex="rowIndex" let-expanded="expanded">
<tr>
<td colspan="5">
<button
type="button"
pButton
pRipple
[pRowToggler]="customer"
text
rounded
plain
class="mr-2"
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'">
</button>
<img
[alt]="customer.representative.name"
src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{customer.representative.image}}"
width="32"
style="vertical-align: middle; display: inline-block" />
<span class="font-bold ml-2">{{customer.representative.name}}</span>
</td>
</tr>
</ng-template>
<ng-template #groupfooter let-customer>
<tr class="p-rowgroup-footer">
<td colspan="4" style="text-align: right">Total Customers</td>
<td>{{calculateCustomerTotal(customer.representative.name)}}</td>
</tr>
</ng-template>
<ng-template #rowexpansion let-customer>
<tr>
<td>
{{customer.name}}
</td>
<td>
<div class="flex items-center gap-2">
<img src="https://primefaces.org/cdn/primeng/images/demo/flag/flag_placeholder.png" [class]="'flag flag-' + customer.country.code" style="width: 20px" />
<span>{{ customer.country.name }}</span>
</div>
</td>
<td>
{{customer.company}}
</td>
<td>
<p-tag [value]="customer.status" [severity]="getSeverity(customer.status)" />
</td>
<td>
{{customer.date}}
</td>
</tr>
</ng-template>
</p-table>
When rowGroupMode is configured to be rowspan, the grouping column spans multiple rows.
<p-table
[value]="customers"
rowGroupMode="rowspan"
groupRowsBy="representative.name"
sortField="representative.name"
sortMode="single"
[tableStyle]="{'min-width': '75rem'}">
<ng-template #header>
<tr>
<th style="width:3rem">#</th>
<th>Representative</th>
<th>Name</th>
<th>Country</th>
<th>Company</th>
<th>Status</th>
<th>Date</th>
</tr>
</ng-template>
<ng-template
#body
let-customer
let-rowIndex="rowIndex"
let-rowgroup="rowgroup"
let-rowspan="rowspan">
<tr>
<td>{{rowIndex}}</td>
<td *ngIf="rowgroup" [attr.rowspan]="rowspan">
<div class="flex items-center gap-2">
<img [alt]="customer.representative.name" src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ customer.representative.image }}" width="32" />
<span>{{ customer.representative.name }}</span>
</div>
</td>
<td>
{{customer.name}}
</td>
<td>
<div class="flex items-center gap-2">
<img src="https://primefaces.org/cdn/primeng/images/demo/flag/flag_placeholder.png" [class]="'flag flag-' + customer.country.code" style="width: 20px" />
<span>{{ customer.country.name }}</span>
</div>
</td>
<td>
{{customer.company}}
</td>
<td>
<p-tag [value]="customer.status" [severity]="getSeverity(customer.status)" />
</td>
</tr>
</ng-template>
</p-table>
Columns can be resized using drag drop by setting the resizableColumns to true. Fit mode is the default one and the overall table width does not change when a column is resized.
<p-table
[value]="products"
[resizableColumns]="true"
styleClass="p-datatable-gridlines"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th pResizableColumn>Code</th>
<th pResizableColumn>Name</th>
<th pResizableColumn>Category</th>
<th pResizableColumn>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{product.code}}</td>
<td>{{product.name}}</td>
<td>{{product.category}}</td>
<td>{{product.quantity}}</td>
</tr>
</ng-template>
</p-table>
Setting columnResizeMode as expand changes the table width as well.
<p-table
[value]="products"
[resizableColumns]="true"
columnResizeMode="expand"
styleClass="p-datatable-gridlines"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th pResizableColumn>Code</th>
<th pResizableColumn>Name</th>
<th pResizableColumn>Category</th>
<th pResizableColumn>Quantity</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td>{{product.code}}</td>
<td>{{product.name}}</td>
<td>{{product.category}}</td>
<td>{{product.quantity}}</td>
</tr>
</ng-template>
</p-table>
<p-table
[value]="customers"
[scrollable]="true"
scrollHeight="400px"
[resizableColumns]="true"
styleClass="p-datatable-gridlines"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th pResizableColumn>Name</th>
<th pResizableColumn>Country</th>
<th pResizableColumn>Company</th>
<th pResizableColumn>Representative</th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr>
<td>{{customer.name}}</td>
<td>{{customer.country.name}}</td>
<td>{{customer.company}}</td>
<td>{{customer.representative.name}}</td>
</tr>
</ng-template>
</p-table>
Order of the columns and rows can be changed using drag and drop. Column reordering is configured by adding reorderableColumns property.
Similarly, adding reorderableRows property enables draggable rows. For the drag handle a column needs to have rowReorder property and onRowReorder callback is required to control the state of the rows after reorder completes.
<p-table
[value]="products"
[columns]="cols"
[reorderableColumns]="true"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header let-columns>
<tr>
<th style="width:3rem"></th>
<th *ngFor="let col of columns" pReorderableColumn>
{{col.header}}
</th>
</tr>
</ng-template>
<ng-template
#body
let-rowData
let-columns="columns"
let-index="rowIndex">
<tr [pReorderableRow]="index">
<td>
<span class="pi pi-bars" pReorderableRowHandle></span>
</td>
<td *ngFor="let col of columns">
{{rowData[col.field]}}
</td>
</tr>
</ng-template>
</p-table>
This demo uses a multiselect component to implement toggleable columns.
<p-table
[columns]="selectedColumns"
[value]="products"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #caption>
<p-multiselect
display="chip"
[options]="cols"
[(ngModel)]="selectedColumns"
optionLabel="header"
selectedItemsLabel="{0} columns selected"
[style]="{'min-width': '200px'}"
placeholder="Choose Columns" />
</ng-template>
<ng-template #header let-columns>
<tr>
<th>Code</th>
<th *ngFor="let col of columns">
{{col.header}}
</th>
</tr>
</ng-template>
<ng-template #body let-product let-columns="columns">
<tr>
<td>{{product.code}}</td>
<td *ngFor="let col of columns">
{{product[col.field]}}
</td>
</tr>
</ng-template>
</p-table>
Table can export its data to CSV format.
<p-table
#dt
[columns]="cols"
[value]="products"
selectionMode="multiple"
[(selection)]="selectedProducts"
[exportHeader]="'customExportHeader'"
[tableStyle]="{ 'min-width': '50rem' }">
<ng-template #caption>
<div class="text-end pb-4">
<p-button
icon="pi pi-external-link"
label="Export"
(click)="dt.exportCSV()" />
</div>
</ng-template>
<ng-template #header let-columns>
<tr>
<th *ngFor="let col of columns">
{{ col.header }}
</th>
</tr>
</ng-template>
<ng-template #body let-rowData let-columns="columns">
<tr [pSelectableRow]="rowData">
<td *ngFor="let col of columns">
{{ rowData[col.field] }}
</td>
</tr>
</ng-template>
</p-table>
Table has exclusive integration with contextmenu component. In order to attach a menu to a table, add pContextMenuRow directive to the rows that can be selected with context menu, define a local template variable for the menu and bind it to the contextMenu property of the table. This enables displaying the menu whenever a row is right clicked. Optional pContextMenuRowIndex property is available to access the row index. A separate contextMenuSelection property is used to get a hold of the right clicked row. For dynamic columns, setting pContextMenuRowDisabled property as true disables context menu for that particular row.
<p-contextmenu #cm [model]="items" (onHide)="selectedProduct = null" />
<p-table
[value]="products"
[(contextMenuSelection)]="selectedProduct"
[contextMenu]="cm"
dataKey="code"
[tableStyle]="{'min-width': '50rem'}">
<ng-template #header>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr [pContextMenuRow]="product">
<td>{{product.code}}</td>
<td>{{product.name}}</td>
<td>{{product.category}}</td>
<td>{{product.price | currency: 'USD'}}</td>
</tr>
</ng-template>
</p-table>
<p-toast />
Stateful table allows keeping the state such as page, sort and filtering either at local storage or session storage so that when the page is visited again, table would render the data using the last settings.
Change the state of the table e.g paginate, navigate away and then return to this table again to test this feature, the setting is set as session with the stateStorage property so that Table retains the state until the browser is closed. Other alternative is local referring to localStorage for an extended lifetime.
<p-table
#dt1
[value]="customers"
[globalFilterFields]="['name', 'country.name', 'representative.name', 'status']"
selectionMode="single"
[(selection)]="selectedCustomers"
dataKey="id"
[tableStyle]="{ 'min-width': '50rem' }"
[rows]="5"
[paginator]="true"
stateStorage="session"
stateKey="statedemo-session"
>
<ng-template #caption>
<p-iconfield iconPosition="left">
<p-inputicon>
<i class="pi pi-search"></i>
</p-inputicon>
<input
pInputText
type="text"
(input)="dt1.filterGlobal($event.target.value, 'contains')"
placeholder="Global Search"
/>
</p-iconfield>
</ng-template>
<ng-template #header>
<tr>
<th pSortableColumn="name" style="width:25%">Name <p-sortIcon field="name" /></th>
<th pSortableColumn="country.name" style="width:25%">
Country <p-sortIcon field="country.name" />
</th>
<th pSortableColumn="representative.name" style="width:25%">
Representative <p-sortIcon field="representative.name" />
</th>
<th pSortableColumn="status" style="width:25%">Status <p-sortIcon field="status" /></th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr [pSelectableRow]="customer">
<td>
{{ customer.name }}
</td>
<td>
<div class="flex items-center gap-2">
<img src="https://primefaces.org/cdn/primeng/images/demo/flag/flag_placeholder.png" [class]="'flag flag-' + customer.country.code" style="width: 20px" />
<span class="ml-1 align-middle">{{ customer.country.name }}</span>
</div>
</td>
<td>
<div class="flex items-center gap-2">
<img [alt]="customer.representative.name" src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ customer.representative.image }}" width="32" style="vertical-align: middle" />
<span class="ml-1 align-middle">{{ customer.representative.name }}</span>
</div>
</td>
<td>
<p-tag [value]="customer.status" [severity]="getSeverity(customer.status)" />
</td>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr [pSelectableRow]="customer">
<td>
{{ customer.name }}
</td>
<td>
<img
src="https://primefaces.org/cdn/primeng/images/demo/flag/flag_placeholder.png"
[class]="'flag flag-' + customer.country.code"
style="width: 20px"
/>
<span class="ml-1 align-middle">{{ customer.country.name }}</span>
</td>
<td>
<img
[alt]="customer.representative.name"
src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{
customer.representative.image
}}"
width="32"
style="vertical-align: middle"
/>
<span class="ml-1 align-middle">{{ customer.representative.name }}</span>
</td>
<td>
<p-tag [value]="customer.status" [severity]="getSeverity(customer.status)" />
</td>
</tr>
</ng-template>
<ng-template emptymessage>
<tr>
<td colspan="4">No customers found.</td>
</tr>
</ng-template>
</p-table>
DataTable with selection, pagination, filtering, sorting and templating.
<p-table
#dt
[value]="customers"
[(selection)]="selectedCustomers"
dataKey="id"
[rowHover]="true"
[rows]="10"
[showCurrentPageReport]="true"
[rowsPerPageOptions]="[10, 25, 50]"
[loading]="loading"
[paginator]="true"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[filterDelay]="0"
[globalFilterFields]="['name', 'country.name', 'representative.name', 'status']"
>
<ng-template #caption>
<div class="flex justify-between">
<p-button [outlined]="true" icon="pi pi-filter-slash" label="Clear" (click)="clear(dt)" />
<p-iconField iconPosition="left">
<p-inputIcon>
<i class="pi pi-search"></i>
</p-inputIcon>
<input pInputText type="text" [(ngModel)]="searchValue" (input)="dt.filterGlobal($event.target.value, 'contains')" placeholder="Keyboard Search" />
</p-iconField>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 4rem">
<p-tableHeaderCheckbox />
</th>
<th pSortableColumn="name" style="min-width: 14rem">
<div class="flex justify-between items-center">
Name
<p-sortIcon field="name" />
<p-columnFilter type="text" field="name" display="menu" class="ml-auto" />
</div>
</th>
<th pSortableColumn="country.name" style="min-width: 14rem">
<div class="flex justify-between items-center">
Country
<p-sortIcon field="country.name" />
<p-columnFilter type="text" field="country.name" display="menu" class="ml-auto" />
</div>
</th>
<th pSortableColumn="representative.name" style="min-width: 14rem">
<div class="flex justify-between items-center">
Agent
<p-sortIcon field="representative.name" />
<p-columnFilter field="representative" matchMode="in" display="menu" [showMatchModes]="false" [showOperator]="false" [showAddButton]="false" class="ml-auto">
<ng-template #filter let-value let-filter="filterCallback">
<p-multiselect [filter]="false" [(ngModel)]="value" [options]="representatives" placeholder="Any" (onChange)="filter($event.value)" optionLabel="name" class="w-full">
<ng-template let-option #item>
<div class="flex items-center gap-2">
<img [alt]="option.label" src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ option.image }}" style="width: 32px" />
<span>{{ option.name }}</span>
</div>
</ng-template>
</p-multiselect>
</ng-template>
</p-columnFilter>
</div>
</th>
<th pSortableColumn="date" style="min-width: 10rem">
<div class="flex justify-between items-center">
Date
<p-sortIcon field="date" />
<p-columnFilter type="date" field="date" display="menu" class="ml-auto" />
</div>
</th>
<th pSortableColumn="balance" style="min-width: 10rem">
<div class="flex justify-between items-center">
Balance
<p-sortIcon field="balance" />
<p-columnFilter type="numeric" field="balance" display="menu" currency="USD" class="ml-auto" />
</div>
</th>
<th pSortableColumn="status" style="min-width: 10rem">
<div class="flex justify-between items-center">
Status
<p-sortIcon field="status" />
<p-columnFilter field="status" matchMode="equals" display="menu" class="ml-auto">
<ng-template #filter let-value let-filter="filterCallback">
<p-dropdown [(ngModel)]="value" [options]="statuses" (onChange)="filter($event.value)" placeholder="Any">
<ng-template let-option #item>
<p-tag [value]="option.label" [severity]="getSeverity(option.label)" />
</ng-template>
</p-dropdown>
</ng-template>
</p-columnFilter>
</div>
</th>
<th pSortableColumn="activity" style="min-width: 10rem">
<div class="flex justify-between items-center">
Activity
<p-sortIcon field="activity" />
<p-columnFilter field="activity" matchMode="between" display="menu" [showMatchModes]="false" [showOperator]="false" [showAddButton]="false" class="ml-auto">
<ng-template #filter let-filter="filterCallback">
<p-slider [(ngModel)]="activityValues" [range]="true" (onSlideEnd)="filter($event.values)" styleClass="m-4"></p-slider>
<div class="flex items-center justify-between px-2">
<span>{{ activityValues[0] }}</span>
<span>{{ activityValues[1] }}</span>
</div>
</ng-template>
</p-columnFilter>
</div>
</th>
<th style="width: 5rem"></th>
</tr>
</ng-template>
<ng-template #body let-customer>
<tr class="p-selectable-row">
<td>
<p-tableCheckbox [value]="customer" />
</td>
<td>
{{ customer.name }}
</td>
<td>
<div class="flex items-center gap-2">
<img src="https://primefaces.org/cdn/primeng/images/demo/flag/flag_placeholder.png" [class]="'flag flag-' + customer.country.code" style="width: 20px" />
<span class="ml-1 align-middle">{{ customer.country.name }}</span>
</div>
</td>
<td>
<div class="flex items-center gap-2">
<img [alt]="customer.representative.name" src="https://primefaces.org/cdn/primeng/images/demo/avatar/{{ customer.representative.image }}" width="32" style="vertical-align: middle" />
<span class="ml-1 align-middle">{{ customer.representative.name }}</span>
</div>
</td>
<td>
{{ customer.date | date: 'MM/dd/yyyy' }}
</td>
<td>
{{ customer.balance | currency: 'USD' : 'symbol' }}
</td>
<td>
<p-tag [value]="customer.status" [severity]="getSeverity(customer.status)" />
</td>
<td>
<p-progressBar [value]="customer.activity" [showValue]="false" />
</td>
<td style="text-align: center">
<p-button rounded icon="pi pi-cog" />
</td>
</tr>
</ng-template>
<ng-template #emptymessage>
<tr>
<td colspan="8">No customers found.</td>
</tr>
</ng-template>
</p-table>
CRUD implementation example with a Dialog.
<p-toolbar styleClass="mb-6">
<ng-template #start>
<p-button label="New" icon="pi pi-plus" class="mr-2" (onClick)="openNew()" />
<p-button severity="danger" label="Delete" icon="pi pi-trash" outlined (onClick)="deleteSelectedProducts()" [disabled]="!selectedProducts || !selectedProducts.length" />
</ng-template>
<ng-template #end>
<p-fileUpload mode="basic" accept="image/*" [maxFileSize]="1000000" label="Import" chooseLabel="Import" auto customUpload class="mr-2 inline-block" [chooseButtonProps]="{ severity: 'secondary' }" />
<p-button label="Export" icon="pi pi-upload" severity="secondary" (onClick)="exportCSV($event)" />
</ng-template>
</p-toolbar>
<p-table
#dt
[value]="products"
[rows]="10"
[columns]="cols"
[paginator]="true"
[globalFilterFields]="['name', 'country.name', 'representative.name', 'status']"
[tableStyle]="{ 'min-width': '75rem' }"
[(selection)]="selectedProducts"
[rowHover]="true"
dataKey="id"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true"
>
<ng-template #caption>
<div class="flex items-center justify-between">
<h5 class="m-0">Manage Products</h5>
<p-iconfield>
<p-inputicon styleClass="pi pi-search" />
<input pInputText type="text" (input)="dt.filterGlobal($event.target.value, 'contains')" placeholder="Search..." />
</p-iconfield>
</div>
</ng-template>
<ng-template #header>
<tr>
<th style="width: 3rem">
<p-tableHeaderCheckbox />
</th>
<th style="min-width: 16rem">Code</th>
<th pSortableColumn="name" style="min-width:16rem">
Name
<p-sortIcon field="name" />
</th>
<th>Image</th>
<th pSortableColumn="price" style="min-width: 8rem">
Price
<p-sortIcon field="price" />
</th>
<th pSortableColumn="category" style="min-width:10rem">
Category
<p-sortIcon field="category" />
</th>
<th pSortableColumn="rating" style="min-width: 12rem">
Reviews
<p-sortIcon field="rating" />
</th>
<th pSortableColumn="inventoryStatus" style="min-width: 12rem">
Status
<p-sortIcon field="inventoryStatus" />
</th>
<th style="min-width: 12rem"></th>
</tr>
</ng-template>
<ng-template #body let-product>
<tr>
<td style="width: 3rem">
<p-tableCheckbox [value]="product" />
</td>
<td style="min-width: 12rem">{{ product.code }}</td>
<td style="min-width: 16rem">{{ product.name }}</td>
<td>
<img [src]="'https://primefaces.org/cdn/primeng/images/demo/product/' + product.image" [alt]="product.name" style="width: 64px" class="rounded" />
</td>
<td>{{ product.price | currency: 'USD' }}</td>
<td>{{ product.category }}</td>
<td>
<p-rating [(ngModel)]="product.rating" [readonly]="true" [cancel]="false" />
</td>
<td>
<p-tag [value]="product.inventoryStatus" [severity]="getSeverity(product.inventoryStatus)" />
</td>
<td>
<p-button icon="pi pi-pencil" class="mr-2" [rounded]="true" [outlined]="true" (click)="editProduct(product)" />
<p-button icon="pi pi-trash" severity="danger" [rounded]="true" [outlined]="true" (click)="deleteProduct(product)" />
</td>
</tr>
</ng-template>
<ng-template #summary>
<div class="flex items-center justify-between">In total there are {{ products ? products.length : 0 }} products.</div>
</ng-template>
</p-table>
<p-dialog [(visible)]="productDialog" [style]="{ width: '450px' }" header="Product Details" [modal]="true">
<ng-template #content>
<div class="flex flex-col gap-6">
<img [src]="'https://primefaces.org/cdn/primeng/images/demo/product/' + product.image" [alt]="product.image" class="block m-auto pb-4" *ngIf="product.image" />
<div>
<label for="name" class="block font-bold mb-3">Name</label>
<input type="text" pInputText id="name" [(ngModel)]="product.name" required autofocus fluid />
<small class="text-red-500" *ngIf="submitted && !product.name">Name is required.</small>
</div>
<div>
<label for="description" class="block font-bold mb-3">Description</label>
<textarea id="description" pTextarea [(ngModel)]="product.description" required rows="3" cols="20" fluid></textarea>
</div>
<div>
<label for="inventoryStatus" class="block font-bold mb-3">Inventory Status</label>
<p-select [(ngModel)]="product.inventoryStatus" inputId="inventoryStatus" [options]="statuses" optionLabel="label" placeholder="Select a Status" fluid />
</div>
<div>
<span class="block font-bold mb-4">Category</span>
<div class="grid grid-cols-12 gap-4">
<div class="flex items-center gap-2 col-span-6">
<p-radiobutton id="category1" name="category" value="Accessories" [(ngModel)]="product.category" />
<label for="category1">Accessories</label>
</div>
<div class="flex items-center gap-2 col-span-6">
<p-radiobutton id="category2" name="category" value="Clothing" [(ngModel)]="product.category" />
<label for="category2">Clothing</label>
</div>
<div class="flex items-center gap-2 col-span-6">
<p-radiobutton id="category3" name="category" value="Electronics" [(ngModel)]="product.category" />
<label for="category3">Electronics</label>
</div>
<div class="flex items-center gap-2 col-span-6">
<p-radiobutton id="category4" name="category" value="Fitness" [(ngModel)]="product.category" />
<label for="category4">Fitness</label>
</div>
</div>
</div>
<div class="grid grid-cols-12 gap-4">
<div class="col-span-6">
<label for="price" class="block font-bold mb-3">Price</label>
<p-inputnumber id="price" [(ngModel)]="product.price" mode="currency" currency="USD" locale="en-US" fluid />
</div>
<div class="col-span-6">
<label for="quantity" class="block font-bold mb-3">Quantity</label>
<p-inputnumber id="quantity" [(ngModel)]="product.quantity" fluid />
</div>
</div>
</div>
</ng-template>
<ng-template #footer>
<p-button label="Cancel" icon="pi pi-times" text (click)="hideDialog()" />
<p-button label="Save" icon="pi pi-check" (click)="saveProduct()" />
</ng-template>
</p-dialog>
<p-confirmDialog [style]="{ width: '450px' }" />
Default role of the table is table. Header, body and footer elements use rowgroup, rows use row role, header cells have columnheader and body cells use cell roles. Sortable headers utilizer aria-sort attribute either set to "ascending" or "descending".
Table rows and table cells should be specified by users using the aria-posinset, aria-setsize, aria-label, and aria-describedby attributes, as they are determined through templating.
Built-in checkbox and radiobutton components for row selection use checkbox and radiobutton. The label to describe them is retrieved from the aria.selectRow and aria.unselectRow properties of the locale API. Similarly header checkbox uses selectAll and unselectAll keys. When a row is selected, aria-selected is set to true on a row.
The element to expand or collapse a row is a button with aria-expanded and aria-controls properties. Value to describe the buttons is derived from aria.expandRow and aria.collapseRow properties of the locale API.
The filter menu button use aria.showFilterMenu and aria.hideFilterMenu properties as aria-label in addition to the aria-haspopup, aria-expanded and aria-controls to define the relation between the button and the overlay. Popop menu has dialog role with aria-modal as focus is kept within the overlay. The operator dropdown use aria.filterOperator and filter constraints dropdown use aria.filterConstraint properties. Buttons to add rules on the other hand utilize aria.addRule and aria.removeRule properties. The footer buttons similarly use aria.clear and aria.apply properties. filterInputProps of the Column component can be used to define aria labels for the built-in filter components, if a custom component is used with templating you also may define your own aria labels as well.
Editable cells use custom templating so you need to manage aria roles and attributes manually if required. The row editor controls are button elements with aria.editRow, aria.cancelEdit and aria.saveEdit used for the aria-label.
Paginator is a standalone component used inside the Table, refer to the paginator for more information about the accessibility features.
Any button element inside the Table used for cases like filter, row expansion, edit are tabbable and can be used with space and enter keys.
Key | Function |
---|---|
tab | Moves through the headers. |
enter | Sorts the column. |
space | Sorts the column. |
Key | Function |
---|---|
tab | Moves through the elements inside the popup. |
escape | Hides the popup. |
enter | Opens the popup. |
Key | Function |
---|---|
tab | Moves focus to the first selected row, if there is none then first row receives the focus. |
up arrow | Moves focus to the previous row. |
down arrow | Moves focus to the next row. |
enter | Toggles the selected state of the focused row depending on the metaKeySelection setting. |
space | Toggles the selected state of the focused row depending on the metaKeySelection setting. |
home | Moves focus to the first row. |
end | Moves focus to the last row. |
shift + down arrow | Moves focus to the next row and toggles the selection state. |
shift + up arrow | Moves focus to the previous row and toggles the selection state. |
shift + space | Selects the rows between the most recently selected row and the focused row. |
control + shift + home | Selects the focused rows and all the options up to the first one. |
control + shift + end | Selects the focused rows and all the options down to the last one. |
control + a | Selects all rows. |