| Name |
Position |
Office |
Age |
Start date |
Salary |
| Tiger Nixon |
System Architect |
Edinburgh |
61 |
2011/04/25 |
$320,800 |
| Garrett Winters |
Accountant |
Tokyo |
63 |
2011/07/25 |
$170,750 |
<table class="display" id="basic-2">
<form>
<div class="mb-3 row">
<label for="table-complete-search" class="col-xs-3 col-sm-auto col-form-label">Full text search:</label>
<div class="col-xs-3 col-sm-auto">
<input
id="table-complete-search"
type="text"
class="form-control"
name="searchTerm"
[(ngModel)]="service.searchTerm"
/>
</div>
@if (service.loading$ | async) {
<span class="col col-form-label">Loading...</span>
}
</div>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" sortable="name" (sort)="onSort($event)">Country</th>
<th scope="col" sortable="area" (sort)="onSort($event)">Area</th>
<th scope="col" sortable="population" (sort)="onSort($event)">Population</th>
</tr>
</thead>
<tbody>
@for (country of countries$ | async; track country.id) {
<tr>
<th scope="row">{{ country.id }}</th>
<td>
<img
[src]="'https://upload.wikimedia.org/wikipedia/commons/' + country.flag"
[alt]="'The flag of ' + country.name"
class="me-2"
style="width: 20px"
/>
<ngb-highlight [result]="country.name" [term]="service.searchTerm" />
</td>
<td><ngb-highlight [result]="country.area | number" [term]="service.searchTerm" /></td>
<td><ngb-highlight [result]="country.population | number" [term]="service.searchTerm" /></td>
</tr>
} @empty {
<tr>
<td colspan="4" style="text-align: center">No countries found</td>
</tr>
}
</tbody>
</table>
<div class="d-flex justify-content-between p-2">
<ngb-pagination [collectionSize]="(total$ | async)!" [(page)]="service.page" [pageSize]="service.pageSize">
</ngb-pagination>
<select class="form-select" style="width: auto" name="pageSize" [(ngModel)]="service.pageSize">
<option [ngValue]="2">2 items per page</option>
<option [ngValue]="4">4 items per page</option>
<option [ngValue]="6">6 items per page</option>
</select>
</div>
</form>
ts files
import { AsyncPipe, DecimalPipe } from '@angular/common';
import { Component, QueryList, ViewChildren } from '@angular/core';
import { Observable } from 'rxjs';
import { Country } from './country';
import { CountryService } from './country.service';
import { NgbdSortableHeader, SortEvent } from './sortable.directive';
import { FormsModule } from '@angular/forms';
import { NgbHighlight, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'ngbd-table-complete',
imports: [DecimalPipe, FormsModule, AsyncPipe, NgbHighlight, NgbdSortableHeaderDirective, NgbPaginationModule],
templateUrl: './table-complete.html',
providers: [CountryService, DecimalPipe],
})
export class NgbdTableComplete {
countries$: Observable;
total$: Observable;
readonly headers = viewChildren(NgbdSortableHeaderDirective);
public service = inject(CountryService);
constructor() {
this.countries$ = service.countries$;
this.total$ = service.total$;
}
onSort({ column, direction }: SortEvent) {
this.headers().forEach(header => {
if (header.sortable() !== column) {
header.direction = '';
}
});
this.direction = direction;
this.service.sortColumn = column;
this.service.sortDirection = direction;
}
}
service file
import { Injectable, PipeTransform } from '@angular/core';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { Country } from './country';
import { COUNTRIES } from './countries';
import { DecimalPipe } from '@angular/common';
import { debounceTime, delay, switchMap, tap } from 'rxjs/operators';
import { SortColumn, SortDirection } from './sortable.directive';
interface SearchResult {
countries: Country[];
total: number;
}
interface State {
page: number;
pageSize: number;
searchTerm: string;
sortColumn: SortColumn;
sortDirection: SortDirection;
}
const compare = (v1: string | number, v2: string | number) => (v1 < v2 ? -1 : v1 > v2 ? 1 : 0);
function sort(countries: Country[], column: SortColumn, direction: string): Country[] {
if (direction === '' || column === '') {
return countries;
} else {
return [...countries].sort((a, b) => {
const res = compare(a[column], b[column]);
return direction === 'asc' ? res : -res;
});
}
}
function matches(country: Country, term: string, pipe: PipeTransform) {
return (
country.name.toLowerCase().includes(term.toLowerCase()) ||
pipe.transform(country.area).includes(term) ||
pipe.transform(country.population).includes(term)
);
}
@Injectable({ providedIn: 'root' })
export class CountryService {
private pipe = inject(DecimalPipe);
private _loading$ = new BehaviorSubject(true);
private _search$ = new Subject();
private _countries$ = new BehaviorSubject([]);
private _total$ = new BehaviorSubject(0);
private _state: State = {
page: 1,
pageSize: 4,
searchTerm: '',
sortColumn: '',
sortDirection: '',
};
constructor() {
this._search$
.pipe(
tap(() => this._loading$.next(true)),
debounceTime(200),
switchMap(() => this._search()),
delay(200),
tap(() => this._loading$.next(false)),
)
.subscribe((result) => {
this._countries$.next(result.countries);
this._total$.next(result.total);
});
this._search$.next();
}
get countries$() {
return this._countries$.asObservable();
}
get total$() {
return this._total$.asObservable();
}
get loading$() {
return this._loading$.asObservable();
}
get page() {
return this._state.page;
}
get pageSize() {
return this._state.pageSize;
}
get searchTerm() {
return this._state.searchTerm;
}
set page(page: number) {
this._set({ page });
}
set pageSize(pageSize: number) {
this._set({ pageSize });
}
set searchTerm(searchTerm: string) {
this._set({ searchTerm });
}
set sortColumn(sortColumn: SortColumn) {
this._set({ sortColumn });
}
set sortDirection(sortDirection: SortDirection) {
this._set({ sortDirection });
}
private _set(patch: Partial) {
Object.assign(this._state, patch);
this._search$.next();
}
private _search(): Observable {
const { sortColumn, sortDirection, pageSize, page, searchTerm } = this._state;
// 1. sort
let countries = sort(COUNTRIES, sortColumn, sortDirection);
// 2. filter
countries = countries.filter((country) => matches(country, searchTerm, this.pipe));
const total = countries.length;
// 3. paginate
countries = countries.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize);
return of({ countries, total });
}
}
firective file
import { Directive, Input, input, output } from '@angular/core';
import { supportDB } from '../data/support-ticket/support-ticket';
export type SortColumn = keyof supportDB | '';
export type SortDirection = 'asc' | 'desc' | '';
const rotate: { [key: string]: SortDirection } = { asc: 'desc', desc: '', '': 'asc' };
export interface SortEvent {
column: SortColumn;
direction: SortDirection;
}
@Directive({
selector: 'th[sortable]',
host: {
'[class.asc]': 'direction === "asc"',
'[class.desc]': 'direction === "desc"',
'(click)': 'rotate()',
},
})
export class NgbdSortableHeader2Directive {
constructor() {}
readonly sortable = input('');
@Input() direction: SortDirection = '';
readonly sort = output();
rotate() {
this.direction = rotate[this.direction];
this.sort.emit({ column: this.sortable(), direction: this.direction });
}
}
data file
export const COUNTRIES: Country[] = [
{
id: 1,
name: 'Russia',
flag: 'f/f3/Flag_of_Russia.svg',
area: 17075200,
population: 146989754,
},
{
id: 2,
name: 'France',
flag: 'c/c3/Flag_of_France.svg',
area: 640679,
population: 64979548,
},
{
id: 3,
name: 'Germany',
flag: 'b/ba/Flag_of_Germany.svg',
area: 357114,
population: 82114224,
},
{
id: 4,
name: 'Portugal',
flag: '5/5c/Flag_of_Portugal.svg',
area: 92090,
population: 10329506,
},
{
id: 5,
name: 'Canada',
flag: 'c/cf/Flag_of_Canada.svg',
area: 9976140,
population: 36624199,
},
{
id: 6,
name: 'Vietnam',
flag: '2/21/Flag_of_Vietnam.svg',
area: 331212,
population: 95540800,
},
{
id: 7,
name: 'Brazil',
flag: '0/05/Flag_of_Brazil.svg',
area: 8515767,
population: 209288278,
},
{
id: 8,
name: 'Mexico',
flag: 'f/fc/Flag_of_Mexico.svg',
area: 1964375,
population: 129163276,
},
{
id: 9,
name: 'United States',
flag: 'a/a4/Flag_of_the_United_States.svg',
area: 9629091,
population: 324459463,
},
{
id: 10,
name: 'India',
flag: '4/41/Flag_of_India.svg',
area: 3287263,
population: 1324171354,
},
{
id: 11,
name: 'Indonesia',
flag: '9/9f/Flag_of_Indonesia.svg',
area: 1910931,
population: 263991379,
},
{
id: 12,
name: 'Tuvalu',
flag: '3/38/Flag_of_Tuvalu.svg',
area: 26,
population: 11097,
},
{
id: 13,
name: 'China',
flag: 'f/fa/Flag_of_the_People%27s_Republic_of_China.svg',
area: 9596960,
population: 1409517397,
},
];
interface file
export interface Country {
id: number;
name: string;
flag: string;
area: number;
population: number;
}