Custom Angular Table - No Libraries
Custom Angular Table - No Libraries

In this post you will learn how to implement a custom Angular table without any libraries with sorting and filtering.

Finished project

Really often you need to implement a table inside Angular and you might think that you need to find a suitable library for this. For example you could take tables component from Angular Material. But you must understand their documentation, sometimes it is not flexible enough and it is easier to support your own code.

Additionally if you don't have lots of tables it makes a lot of sense to implement it on your own because it is not that difficult. This is exactly what we want to do in this video.

Here I already prepared an API which responds with the array of users.

API response

As you can see we get every user with id, name and age properties. This is exactly the data that we want to render inside our table.

Table module

Here I have an empty Angular project and for our users table I want to create a new module. Why new module? We want to completely isolate our feature with users table inside this module.

Let's add an empty module first.

// src/app/usersTable/usersTable.module.ts
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UsersTableComponent } from './components/usersTable/usersTable.component';

@NgModule({
  imports: [CommonModule],
  declarations: [UsersTableComponent],
  exports: [UsersTableComponent],
})
export class UsersTableModule {}

Here is our new module. We registered inside UsersTableComponent that we will create in a second.

// src/app/usersTable/components/usersTable/usersTable.component.ts
@Component({
  selector: 'users-table',
  templateUrl: './usersTable.component.html',
  styleUrls: ['./usersTable.component.scss'],
})
export class UsersTableComponent implements OnInit {}

And let's add some basic html.

// src/app/usersTable/components/usersTable/usersTable.component.html
<div>Users Table</div>

The last thing that we should not forget is inject UsersTableModule in app.component and render it inside.

// app.module.ts
@NgModule({
  imports: [
    ...
    UsersTableModule,
  ],
})
export class AppModule {}
<users-table></users-table>

As you can see in browser our module is registered and rendered.

Fetching data

Before we start with fetching data I want to create an additional interface for a user. This will help to make our code stricter and more reusable.

/// src/app/usersTable/types/user.interface.ts
export interface UserInterface {
  id: string;
  name: string;
  age: number;
}

With this interface we can create a service to fetch the array of users from our API.

@Injectable()
export class UsersService {
  constructor(private http: HttpClient) {}

  getUsers(): Observable<UserInterface[]> {
    const url = `http://localhost:3004/users`;
    return this.http.get<UserInterface[]>(url);
  }
}

Here we created getUsers method which returns an Observable of users array for us. But we must register this service in our module.

// src/app/usersTable/usersTable.module.ts

@NgModule({
  ...
  providers: [UsersService],
})
export class UsersTableModule {}

Now let's try to fetch our users inside component and check if it's working.

// src/app/usersTable/components/usersTable/usersTable.component.ts
export class UsersTableComponent implements OnInit {
  users: UserInterface[] = [];

  constructor(private usersService: UsersService) {}

  ngOnInit(): void {
    this.fetchData();
  }

  fetchData(): void {
    this.usersService.getUsers().subscribe((users) => {
      this.users = users;
    });
  }
}

Fetched data

As you can see in browser we successfully fetched data from the API and rendered them in our component.

Rendering the table

Now let's start with rendering our table. First thing that we need to do is defining our columns.

// src/app/usersTable/components/usersTable/usersTable.component.ts
export class UsersTableComponent implements OnInit {
  columns: string[] = ['id', 'name', 'age'];
}

Now we can use this columns to render our table.

// src/app/usersTable/components/usersTable/usersTable.component.html
<table>
  <thead>
    <tr>
      <th *ngFor="let column of columns">
        {{ column }}
      </th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let user of users">
      <td *ngFor="let column of columns">
        {{ user[column] }}
      </td>
    </tr>
  </tbody>
</table>

Here we rendered a table with head and body with looping through columns and users. But we are getting an error in terminal.

Error

It says that user[column] is wrong because we can't just take any string from the UserInterface. And as columns is array of strings we get this error.

The easiest fix it to define our columns as an array user keys for user.

export class UsersTableComponent implements OnInit {
  columns: Array<keyof UserInterface> = ['id', 'name', 'age'];
  ...
}

This will fix the error because now we can have only real keys of UserInterface inside.

Basic table

As you can see in browser we successfully rendered the content of our table.

Styling

But now I want to style it a little bit. This is why inside our scss file I want to create styles for the table and for every single cell.

// src/app/usersTable/components/usersTable/usersTable.component.scss
.users-table {
  table-layout: fixed;
  width: 100%;
  border-collapse: collapse;
}

.users-table-cell {
  border: 1px solid #dddddd;
  text-align: left;
  padding: 8px;
}

Now let's apply this styles to our html

<table class="users-table">
  <thead>
    <tr>
      <th
        *ngFor="let column of columns"
        class="users-table-cell"
      >
        {{ column }}
      </th>
    </tr>
  </thead>
  <tbody>
    <tr *ngFor="let user of users">
      <td *ngFor="let column of columns" class="users-table-cell">
        {{ user[column] }}
      </td>
    </tr>
  </tbody>
</table>

As you can see we added users-table to our table element and users-table-cell for each th and td.

Styled table

Now our table looks much better.

Capitalize title

But it is not all. I really want to capitalize the names in our header. For this we can create an additional function.

export class UsersTableComponent implements OnInit {
  ...
  capitalize(str: string): string {
    return str.charAt(0).toUpperCase() + str.substring(1);
  }
}

Actually this function is fully reusable and is not related only to UsersTable but as we have just a single feature it is fine to write it here.

Let's use this function in our template.

<th
  *ngFor="let column of columns"
  class="users-table-cell"
>
  {{ capitalize(column) }}
</th>

As you can see in browser our header cells are nicely capitalized.

Sorting table

We successfully fetched the data from the API and rendered them inside the table. But typically you also want to sort this data and every single time when we click on the header you want to refetch data that are sorted differently.

But for this I want to create a new type.

/// src/app/usersTable/types/sorting.interface.ts
export interface SortingInterface {
  column: string;
  order: 'asc' | 'desc';
}

This interface will help us to work with sorting. As you can see it contains the column that we sort and the order of sorting.

Now let's add a default sorting in our component.

export class UsersTableComponent implements OnInit {
  ...
  sorting: SortingInterface = {
    column: 'id',
    order: 'asc',
  };
}

Because we set the default sorting in our component we will never have null and it simplifies our code tremendously.

Let's render arrows up and down to show the current sorting.

<th
  *ngFor="let column of columns"
  class="users-table-cell"
  (click)="sortTable(column)"
>
  {{ capitalize(column) }}
  <span *ngIf="isDescSorting(column)"></span>
  <span *ngIf="isAscSorting(column)"></span>
</th>

Here we just render 2 icons if the descending or ascending sorting is activated for this column.

export class UsersTableComponent implements OnInit {
  ...
  isDescSorting(column: string): boolean {
    return this.sorting.column === column && this.sorting.order === 'desc';
  }

  isAscSorting(column: string): boolean {
    return this.sorting.column === column && this.sorting.order === 'asc';
  }
}

Here we define this 2 methods. We just compare if we are sorting this column and if the order is like we need.

Default sorting

As you can see in browser we successfully rendered sorting on the screen.

Now we want to change our sorting. For this we need to attach a click event to every header cell.

<th
  ...
  (click)="sortTable(column)"
>
  ...
</th>

Here we call sortTable where we pass what column to want to use for sorting.

export class UsersTableComponent implements OnInit {
  ...
  sortTable(column: string): void {
    const futureSortingOrder = this.isDescSorting(column) ? 'asc' : 'desc';
    this.sorting = {
      column,
      order: futureSortingOrder,
    };
    this.fetchData();
  }
}

In this sortTable method we first calculate our future order. If now column is sorted descending then we set it to ascending. After this we set new sorting and call fetchData again.

As you can see in browser we change now our sorting and the icon renders correctly. But our data stays the same. Yes we refetch them but we don't provide sorting inside our API call so nothing is changed.

First of all let's tune our fetchData.

export class UsersTableComponent implements OnInit {
  ...
  fetchData(): void {
    this.usersService.getUsers(this.sorting).subscribe((users) => {
      this.users = users;
    });
  }
}

Our getUsers method doesn't accept sorting yet so we must change that also.

export class UsersService {
  getUsers(
    sorting: SortingInterface,
  ): Observable<UserInterface[]> {
    const url = `http://localhost:3004/users?_sort=${sorting.column}&_order=${sorting.order}`;
    return this.http.get<UserInterface[]>(url);
  }
}

We provide sorting and order in the API url and as you can see in browser backend returns for us correctly sorted data every time.

This means that we successfully implemented sorting of our table.

Filtering table

Now it is time to filter our table. For this we must add an form with the input. The easiest way to do it is to use ReactiveFormsModule from Angular. For this we must add it to our module.

// src/app/usersTable/usersTable.module.ts
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [CommonModule, ReactiveFormsModule],
})
export class UsersTableModule {}

Now we need to create a form group for our reactive form.

export class UsersTableComponent implements OnInit {
  searchForm = this.fb.nonNullable.group({
    searchValue: '',
  });

  constructor(private usersService: UsersService, private fb: FormBuilder) {}
  ...
}

Our searchForm as a form with just a single value for the search input. Now let's write markup for our input.

<div>
  <div class="search-bar">
    <form [formGroup]="searchForm" (ngSubmit)="onSearchSubmit()">
      <input
        type="text"
        placeholder="Search..."
        formControlName="searchValue"
      />
    </form>
  </div>
  <table>
    ...
  </table>
</div>

Before our table we created a form annd binded it to our formGroup and formControlName.

As you can see I also added search-bar class to style our form.

.search-bar {
  margin-bottom: 20px;
}

Here we just added some margin to our search form to make it more readable.

Now we must to exactly the same like we did with sorting. We need some local property as a current search value, we must update it every time when we submit our search form and we must provide it inside our API call.

export class UsersTableComponent implements OnInit {
  searchValue: string = '';

  onSearchSubmit(): void {
    this.searchValue = this.searchForm.value.searchValue ?? '';
    this.fetchData();
  }

  fetchData(): void {
    this.usersService.getUsers(this.sorting, this.searchValue).subscribe((users) => {
      this.users = users;
    });
  }
  ...
}

Here we created searchValue. In our onSearchSubmit which happens when we submit our form we change this property to what is typed inside the form. After this we call our fetchData to refetch filtered users.

As you can see we provided in our getUsers method not only sorting but also our searchValue. This is why now we must tune our getUsers method to pass this data to our API url.

export class UsersService {
  getUsers(
    sorting: SortingInterface,
    searchValue: string
  ): Observable<UserInterface[]> {
    const url = `http://localhost:3004/users?_sort=${sorting.column}&_order=${sorting.order}&name_like=${searchValue}`;
    return this.http.get<UserInterface[]>(url);
  }
}

Inside our url we passed name_like which works like contains in Javascript. We don't filter just with 100% equality but we check if what we are trying to find fits and name.

Filtering

As you can see in browser we can filter our data correctly. So this is how you can implement custom table with filtering and sorting.

And actually if you want to learn Angular with NgRx from empty folder to a fully functional production application make sure to check my Angular and NgRx - Building Real Project From Scratch course.

📚 Source code of what we've done