How to create a table in Angular
Creating data tables is essential for displaying structured information in Angular applications. As the creator of CoreUI with over 10 years of Angular experience since 2014, I’ve built table components for everything from simple lists to complex enterprise data grids with thousands of rows. The most effective approach uses Angular’s structural directives for rendering, pipes for data transformation, and component state for interactive features like sorting and filtering. This pattern provides a professional, performant table interface.
Create a basic table component with static data.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
interface User {
id: number
name: string
email: string
role: string
}
@Component({
selector: 'app-user-table',
standalone: true,
imports: [CommonModule],
template: `
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
</tr>
</tbody>
</table>
`,
styles: [`
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #f5f5f5;
font-weight: bold;
}
tr:hover {
background: #f9f9f9;
}
`]
})
export class UserTableComponent {
users: User[] = [
{ id: 1, name: 'John Doe', email: '[email protected]', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', role: 'User' },
{ id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'User' }
]
}
The *ngFor directive iterates over the users array and creates table rows. The template displays each user’s properties. The styles provide basic table formatting with borders and hover effects. This creates a functional data table.
Adding Sortable Columns
Implement column sorting with click handlers.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
type SortDirection = 'asc' | 'desc' | null
@Component({
selector: 'app-sortable-table',
standalone: true,
imports: [CommonModule],
template: `
<table>
<thead>
<tr>
<th (click)="sort('id')">
ID <span *ngIf="sortColumn === 'id'">{{ sortDirection === 'asc' ? '↑' : '↓' }}</span>
</th>
<th (click)="sort('name')">
Name <span *ngIf="sortColumn === 'name'">{{ sortDirection === 'asc' ? '↑' : '↓' }}</span>
</th>
<th (click)="sort('email')">
Email <span *ngIf="sortColumn === 'email'">{{ sortDirection === 'asc' ? '↑' : '↓' }}</span>
</th>
<th (click)="sort('role')">
Role <span *ngIf="sortColumn === 'role'">{{ sortDirection === 'asc' ? '↑' : '↓' }}</span>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of sortedUsers">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
</tr>
</tbody>
</table>
`,
styles: [`
th {
cursor: pointer;
user-select: none;
}
th:hover {
background: #e0e0e0;
}
`]
})
export class SortableTableComponent {
users = [
{ id: 1, name: 'John Doe', email: '[email protected]', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', role: 'User' },
{ id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'User' }
]
sortColumn: keyof typeof this.users[0] | null = null
sortDirection: SortDirection = null
get sortedUsers() {
if (!this.sortColumn || !this.sortDirection) {
return this.users
}
return [...this.users].sort((a, b) => {
const aValue = a[this.sortColumn!]
const bValue = b[this.sortColumn!]
if (aValue < bValue) return this.sortDirection === 'asc' ? -1 : 1
if (aValue > bValue) return this.sortDirection === 'asc' ? 1 : -1
return 0
})
}
sort(column: keyof typeof this.users[0]) {
if (this.sortColumn === column) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'
} else {
this.sortColumn = column
this.sortDirection = 'asc'
}
}
}
The sort method handles column clicks and toggles sort direction. The sortedUsers getter returns sorted data. Arrow indicators show current sort state. The spread operator creates a new array to avoid mutating the original. Clicking headers alternates between ascending and descending order.
Adding Search and Filtering
Implement text search across all columns.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { FormsModule } from '@angular/forms'
@Component({
selector: 'app-filterable-table',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="table-controls">
<input
type="text"
[(ngModel)]="searchTerm"
placeholder="Search..."
class="search-input"
/>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of filteredUsers">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
</tr>
</tbody>
</table>
<div *ngIf="filteredUsers.length === 0" class="no-results">
No results found
</div>
`,
styles: [`
.table-controls {
margin-bottom: 1rem;
}
.search-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
width: 300px;
}
.no-results {
padding: 2rem;
text-align: center;
color: #666;
}
`]
})
export class FilterableTableComponent {
users = [
{ id: 1, name: 'John Doe', email: '[email protected]', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: '[email protected]', role: 'User' },
{ id: 3, name: 'Bob Johnson', email: '[email protected]', role: 'User' }
]
searchTerm = ''
get filteredUsers() {
if (!this.searchTerm) {
return this.users
}
const term = this.searchTerm.toLowerCase()
return this.users.filter(user =>
user.name.toLowerCase().includes(term) ||
user.email.toLowerCase().includes(term) ||
user.role.toLowerCase().includes(term) ||
user.id.toString().includes(term)
)
}
}
The search input binds to searchTerm using [(ngModel)]. The filteredUsers getter filters users based on the search term. The filter checks all columns for matches. The no-results message displays when no users match. This provides instant search feedback.
Adding Pagination
Implement pagination for large datasets.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
@Component({
selector: 'app-paginated-table',
standalone: true,
imports: [CommonModule],
template: `
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of paginatedUsers">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button (click)="previousPage()" [disabled]="currentPage === 1">
Previous
</button>
<span>Page {{ currentPage }} of {{ totalPages }}</span>
<button (click)="nextPage()" [disabled]="currentPage === totalPages">
Next
</button>
<select [(ngModel)]="pageSize" (change)="onPageSizeChange()">
<option [value]="5">5 per page</option>
<option [value]="10">10 per page</option>
<option [value]="25">25 per page</option>
</select>
</div>
`,
styles: [`
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-top: 1rem;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`]
})
export class PaginatedTableComponent {
users = Array.from({ length: 50 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
role: i % 3 === 0 ? 'Admin' : 'User'
}))
currentPage = 1
pageSize = 10
get totalPages() {
return Math.ceil(this.users.length / this.pageSize)
}
get paginatedUsers() {
const start = (this.currentPage - 1) * this.pageSize
const end = start + this.pageSize
return this.users.slice(start, end)
}
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
}
}
previousPage() {
if (this.currentPage > 1) {
this.currentPage--
}
}
onPageSizeChange() {
this.currentPage = 1
}
}
The paginatedUsers getter slices the users array based on current page. The totalPages calculates how many pages are needed. Navigation buttons move between pages. The page size selector allows users to adjust items per page. Changing page size resets to page 1.
Loading Data from API
Fetch table data from a backend API.
import { Component, OnInit } from '@angular/core'
import { CommonModule } from '@angular/common'
import { HttpClient, HttpClientModule } from '@angular/common/http'
@Component({
selector: 'app-api-table',
standalone: true,
imports: [CommonModule, HttpClientModule],
template: `
<div *ngIf="loading" class="loading">Loading...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<table *ngIf="!loading && !error">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role }}</td>
</tr>
</tbody>
</table>
`
})
export class ApiTableComponent implements OnInit {
users: any[] = []
loading = true
error: string | null = null
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get<any[]>('/api/users').subscribe({
next: (data) => {
this.users = data
this.loading = false
},
error: (err) => {
this.error = 'Failed to load users'
this.loading = false
console.error(err)
}
})
}
}
The HttpClient fetches data from the API. The loading state shows a loading indicator. The error state displays error messages. The subscribe method handles success and error responses. The table renders only after data loads successfully.
Adding Row Actions
Include edit and delete buttons for each row.
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
@Component({
selector: 'app-table-with-actions',
standalone: true,
imports: [CommonModule],
template: `
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td class="actions">
<button (click)="editUser(user)" class="btn-edit">Edit</button>
<button (click)="deleteUser(user.id)" class="btn-delete">Delete</button>
</td>
</tr>
</tbody>
</table>
`,
styles: [`
.actions {
display: flex;
gap: 0.5rem;
}
button {
padding: 4px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-edit {
background: #3498db;
color: white;
}
.btn-delete {
background: #e74c3c;
color: white;
}
`]
})
export class TableWithActionsComponent {
users = [
{ id: 1, name: 'John Doe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', email: '[email protected]' }
]
editUser(user: any) {
console.log('Editing user:', user)
// Navigate to edit page or open modal
}
deleteUser(id: number) {
if (confirm('Are you sure you want to delete this user?')) {
this.users = this.users.filter(u => u.id !== id)
}
}
}
The actions column contains edit and delete buttons. The editUser method handles edit actions. The deleteUser method removes users after confirmation. The filter creates a new array without the deleted user. Action buttons trigger appropriate workflows.
Best Practice Note
This is the same table architecture we use in CoreUI Angular templates for data management interfaces. For production applications with large datasets, implement server-side pagination and sorting to reduce client memory usage. Add column visibility toggles for customizable views. Implement row selection with checkboxes for bulk operations. For complex tables with many features, consider using CoreUI’s CTable component or Angular Material’s table which provide advanced features like virtual scrolling, column resizing, and sticky headers out of the box, significantly reducing development time for data-heavy applications.



