Skip to content
Snippets Groups Projects
Commit e5aca475 authored by Jérémy Destin's avatar Jérémy Destin
Browse files

feat: Create the component card-sortable-table to make the sort table more generic. GNP-4309.

parent d97c24cc
No related branches found
No related tags found
1 merge request!46Feat/implement germplasm result page
Showing
with 397 additions and 5 deletions
......@@ -23,12 +23,15 @@ import { CardRowComponent } from './card-row/card-row.component';
import { CardSectionComponent } from './card-section/card-section.component';
import { LoadingSpinnerComponent } from './loading-spinner/loading-spinner.component';
import { CardTableComponent } from './card-table/card-table.component';
import { CardSortableTableComponent } from './card-sortable-table/card-sortable-table.component';
import { MomentModule } from 'ngx-moment';
import { XrefsComponent } from './xrefs/xrefs.component';
import { CoordinatesModule } from 'angular-coordinates';
import { CardGenericDocumentComponent } from './card-generic-document/card-generic-document.component';
import { MarkdownModule, MarkedOptions, MarkedRenderer } from 'ngx-markdown';
import { MarkdownPageComponent } from './markdown-page/markdown-page.component';
import { NgbdSortableHeader } from './card-sortable-table/sortable.directive';
import { DecimalPipe } from '@angular/common';
@NgModule({
declarations: [
......@@ -37,6 +40,7 @@ import { MarkdownPageComponent } from './markdown-page/markdown-page.component';
ResultPageComponent,
GermplasmCardComponent,
GermplasmResultPageComponent,
NgbdSortableHeader,
StudyCardComponent,
SiteCardComponent,
NavbarComponent,
......@@ -50,6 +54,7 @@ import { MarkdownPageComponent } from './markdown-page/markdown-page.component';
CardSectionComponent,
LoadingSpinnerComponent,
CardTableComponent,
CardSortableTableComponent,
XrefsComponent,
CardGenericDocumentComponent,
MarkdownPageComponent
......@@ -86,7 +91,8 @@ import { MarkdownPageComponent } from './markdown-page/markdown-page.component';
],
providers: [
{ provide: HTTP_INTERCEPTORS, useExisting: ErrorInterceptorService, multi: true }
{ provide: HTTP_INTERCEPTORS, useExisting: ErrorInterceptorService, multi: true },
DecimalPipe
],
bootstrap: [AppComponent]
})
......
<p>This is a more complete example with a service that simulates server calling:</p>
<ul>
<li>an observable async service to fetch a list of countries</li>
<li>sorting, filtering and pagination</li>
<li>simulated delay and loading indicator</li>
<li>debouncing of search requests</li>
</ul>
<form>
<div class="form-group form-inline">
Full text search: <input class="form-control ml-2" type="text" name="searchTerm" [(ngModel)]="service.searchTerm"/>
<span class="ml-3" *ngIf="service.loading$ | async">Loading...</span>
</div>
<table class="table table-striped">
<thead>
<tr>
<th scope="col" sortable="{{ header }}" (sort)="onSort($event)" *ngFor="let header of tableHeaders ">{{ header }}</th>
<!--<th *ngFor="let header of tableHeaders " 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>
<tr *ngFor="let country of countries$ | async">
<th scope="row">{{ country.id }}</th>
<td>
<img [src]="'https://upload.wikimedia.org/wikipedia/commons/' + country.flag" class="mr-2" style="width: 20px">
<ngb-highlight [result]="country.name" [term]="service.searchTerm"></ngb-highlight>
</td>
<td><ngb-highlight [result]="country.area | number" [term]="service.searchTerm"></ngb-highlight></td>
<td><ngb-highlight [result]="country.population | number" [term]="service.searchTerm"></ngb-highlight></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="custom-select" style="width: auto" name="pageSize" [(ngModel)]="service.pageSize">
<option [ngValue]="5">5 items per page</option>
<option [ngValue]="10">10 items per page</option>
<option [ngValue]="15">15 items per page</option>
</select>
</div>
</form>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CardSortableTableComponent } from './card-sortable-table.component';
describe('CardSortableTableComponent', () => {
let component: CardSortableTableComponent;
let fixture: ComponentFixture<CardSortableTableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CardSortableTableComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardSortableTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, ContentChild, Input, OnInit, QueryList, TemplateRef, ViewChildren } from '@angular/core';
import { Observable } from 'rxjs';
import { Country } from './country';
import { CountryService } from './country.services';
import { NgbdSortableHeader, SortEvent } from './sortable.directive';
@Component({
selector: 'faidare-card-sortable-table',
templateUrl: './card-sortable-table.component.html',
styleUrls: ['./card-sortable-table.component.scss']
})
export class CardSortableTableComponent {
countries$: Observable<Country[]>;
total$: Observable<number>;
@Input() tableHeaders: String[];
@Input() rows: any;
@ContentChild(TemplateRef) template: TemplateRef<any>;
constructor(public service: CountryService) {
this.countries$ = service.countries$;
this.total$ = service.total$;
}
@ViewChildren(NgbdSortableHeader) headers: QueryList<NgbdSortableHeader>;
onSort({column, direction}: SortEvent) {
// resetting other headers
this.headers.forEach(header => {
console.log(header);
if (header.sortable !== column) {
header.direction = '';
}
});
this.service.sortColumn = column;
this.service.sortDirection = direction;
}
}
import {Country} from './country';
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
}
];
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 {SortDirection} from './sortable.directive';
interface SearchResult {
countries: Country[];
total: number;
}
interface State {
page: number;
pageSize: number;
searchTerm: string;
sortColumn: string;
sortDirection: SortDirection;
}
function compare(v1, v2) {
return v1 < v2 ? -1 : v1 > v2 ? 1 : 0;
}
function sort(countries: Country[], column: string, direction: string): Country[] {
console.log(countries);
console.log(column);
console.log(direction);
if (direction === '') {
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 _loading$ = new BehaviorSubject<boolean>(true);
private _search$ = new Subject<void>();
private _countries$ = new BehaviorSubject<Country[]>([]);
private _total$ = new BehaviorSubject<number>(0);
private _state: State = {
page: 1,
pageSize: 5,
searchTerm: '',
sortColumn: '',
sortDirection: ''
};
constructor(private pipe: DecimalPipe) {
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: string) { this._set({sortColumn}); }
set sortDirection(sortDirection: SortDirection) { this._set({sortDirection}); }
private _set(patch: Partial<State>) {
Object.assign(this._state, patch);
this._search$.next();
}
private _search(): Observable<SearchResult> {
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});
}
}
export interface Country {
id: number;
name: string;
flag: string;
area: number;
population: number;
}
import {Directive, EventEmitter, Input, Output} from '@angular/core';
export type SortDirection = 'asc' | 'desc' | '';
const rotate: {[key: string]: SortDirection} = { 'asc': 'desc', 'desc': '', '': 'asc' };
export interface SortEvent {
column: string;
direction: SortDirection;
}
@Directive({
selector: 'th[sortable]',
host: {
'[class.asc]': 'direction === "asc"',
'[class.desc]': 'direction === "desc"',
'(click)': 'rotate()'
}
})
export class NgbdSortableHeader {
@Input() sortable: string;
@Input() direction: SortDirection = '';
@Output() sort = new EventEmitter<SortEvent>();
rotate() {
this.direction = rotate[this.direction];
this.sort.emit({column: this.sortable, direction: this.direction});
}
}
<p>
germplasm-result-page works!
</p>
<faidare-card-sortable-table
[tableHeaders]="['id','name', 'area', 'population']"
[rows]="countries">
<ng-template let-row>
<tr>
<th scope="row">{{ row.id }}</th>
<td><ngb-highlight [result]="row.name" [term]="row.searchTerm"></ngb-highlight></td>
<td><ngb-highlight [result]="row.area | number" [term]="service.searchTerm"></ngb-highlight></td>
<td><ngb-highlight [result]="row.population | number" [term]="service.searchTerm"></ngb-highlight></td>
</tr>
</ng-template>
</faidare-card-sortable-table>
import { Component, OnInit } from '@angular/core';
import { CountryService } from '../card-sortable-table/country.services';
import { Country } from '../card-sortable-table/country';
@Component({
selector: 'faidare-germplasm-result-page',
......@@ -7,10 +9,18 @@ import { Component, OnInit } from '@angular/core';
})
export class GermplasmResultPageComponent implements OnInit {
constructor() {
countries: Country[];
constructor(public service: CountryService) {
this.service.countries$.subscribe(countries => {
this.countries = countries;
});
}
ngOnInit() {
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment