Reputation: 431
I am using the ngx-pagination package for pagination in my table using Angular 7. I've added the correct directives & pipe operators for my ngFor* which iterates over data provided from a SQL Service I've created.
The pagination controls correctly displays how many items I want ( 15 ) and creates the correct number of pages... yet when I use the controls to click for example, page 2, the items in the table don't change. The items visible are always the first 15 from my 'loadedTableData' array. Is there an additional step I am missing? Or perhaps because I use a nested ngFor* within the original ngFor* this somehow corrupts the pagination? Anyone else seen this before?
data-table.html:
<div id="table" class="table-editable">
<div class="table-container">
<div class="add-row-container">
<form #addRowForm="ngForm" class="add-row-form"
(ngSubmit)="addRow(addRowForm)">
<table #addRowTable class="table
table-bordered table-responsive-md table-striped
text-center">
<thead>
<tr>
<th *ngFor="let head of loadedTableData[0] | keys;">{{head}}</th>
</tr>
</thead>
<tr #newRowTemplate>
<td *ngFor="let property of loadedTableData[0] | keys;"
class="form-group">
<input #prop ngModel
required class="form-control" contenteditable="true"
name="{{property}}">
</td>
</tr>
</table>
<div class="buttons-container">
<button class="btn-success rounded btn-sm my-0 btn"
type="submit"
[disabled]="!addRowForm.valid">Add Row</button>
<button class="btn-outline-primary rounded btn-sm my-0 btn"
type="button"
(click)="addRowForm.resetForm()">Clear</button>
</div>
</form>
</div>
<div class="table-container">
<form #updateRowForm="ngForm" class="update-row-form">
<table #tableEl="" class="table table-bordered
table-responsive-md table-striped text-center">
<thead>
<!-- <tr>
<nav class="navbar">
<input class="form-control" type="text" name="search"
[(ngModel)]="filter">
</nav>
</tr> -->
<tr>
<th> Row </th>
<th *ngFor="let head of loadedTableData[0] | keys;">{{head}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of loadedTableData
| paginate: {
id: 'ltd',
itemsPerPage: 10,
currentPage: p,
totalItems: total
};
let i= index;"
(click)="updatePreviousValue(item);">
<td class="form-group" #rowIndex>
<span> {{ i + 1 }} </span>
</td>
<td *ngFor="let property of item | keys;"
class="form-group" #editRow>
<input #editRowProp
[(ngModel)]="loadedTableData[i][property]"
class="form-control"
[name]="property + '_' + i"
type="text">
</td>
<td>
<button type="button" class="btn btn-primary
rounded
btn-sm my-0"
(click)="updateRow(loadedTableData[i])">Update</button>
<hr>
<button type="button" class="btn btn-danger
rounded
btn-sm my-0" (click)="deleteRow(item)">Remove</button>
</td>
</tr>
</tbody>
<tfoot id="pagination-control-container">
<tr>
<td [colSpan]="99">
<pagination-controls
(pageChange)="pageChange($event);"
id='ltd'>
</pagination-controls>
</td>
</tr>
</tfoot>
</table>
</form>
</div>
</div>
</div>
data-table.component.ts:
import { Component, OnInit, ViewChild, ViewChildren, QueryList, OnDestroy, ChangeDetectorRef, Input } from '@angular/core';
import { SqlService } from '../services/sql.service';
import { NgForm, FormGroup } from '@angular/forms';
import { Subscription, BehaviorSubject } from 'rxjs';
import { MatSnackBar } from '@angular/material';
import { SuccessComponent } from '../snackbar/success/success.component';
import { ErrorComponent } from '../snackbar/error/error.component';
import { ConfirmComponent } from '../snackbar/confirm/confirm.component';
@Component({
selector: 'app-data-table',
templateUrl: './data-table.component.html',
styleUrls: ['./data-table.component.scss']
})
export class DataTableComponent implements OnInit, OnDestroy {
constructor(
private sqlService: SqlService,
private snackBar: MatSnackBar,
private cdRef: ChangeDetectorRef) { }
@ViewChild('addRowForm') addRowForm: NgForm;
@ViewChildren('prop') addRowProps: QueryList<any>;
@ViewChild('editRowForm') editRowForm: NgForm;
@ViewChild('editRow') editRow: FormGroup;
@Input() id: string;
@Input() maxSize: number;
public loadedTableData: any = [];
public previousTableData: object[] = [];
public displayedColumns: object[] = [];
public tableHasBeenLoaded = false;
public rowBeingEdited: BehaviorSubject<any> = new BehaviorSubject<any>({});
public rowPreviousValue = {};
public currentTableData: object = {};
public rowsAffected = 0;
private subscriptions: Subscription[] = [];
public p = 1;
public pageChange(event: number): void {
this.p = event;
}
public ngOnInit(): void {
this.subscriptions.push(
this.sqlService.tableHasBeenLoaded.subscribe(data => {
this.tableHasBeenLoaded = data;
}),
this.sqlService.currentDataView.subscribe(data => {
this.loadedTableData = data;
if (data.length > 0) {
this.displayedColumns.push(Object.getOwnPropertyNames(data[0]));
}
}),
this.sqlService.tableHasBeenLoaded.subscribe(data => {
this.tableHasBeenLoaded = data;
}),
this.sqlService.currentTableData.subscribe(data => {
this.cdRef.detectChanges();
this.currentTableData = data;
}),
this.sqlService.rowsAffected.subscribe(data => {
this.rowsAffected = data;
})
);
}
public addRow(addRowData: NgForm): void {
const newDataValues = [];
const loadedValues = {};
let newDataKeys: object;
const tableData = {
row: addRowData.value,
currentTableData: this.currentTableData
};
this.subscriptions.push(
this.sqlService.insertTableData(tableData)
.subscribe((resp) => {
if (resp) {
for (const prop of this.addRowProps.toArray()) {
newDataValues.push(prop['nativeElement'].value as HTMLInputElement);
}
newDataKeys = Object.keys(addRowData.controls);
Object.assign(newDataKeys).map((key, i) => {
loadedValues[key] = newDataValues[i];
});
if (this.loadedTableData.length > 0) {
const newRow = loadedValues;
this.loadedTableData.push(newRow);
}
this.snackBar.openFromComponent(SuccessComponent, {
duration: 3000,
data: `${this.rowsAffected} row(s) added.`
});
this.addRowForm.resetForm();
}
}, (err) => {
this.snackBar.openFromComponent(ErrorComponent, {
duration: 6000,
data: `${err}`
});
})
);
}
public updatePreviousValue(item: object): void {
this.rowPreviousValue = JSON.parse(JSON.stringify(item));
}
public updateRow(newRowValue: object): void {
const previousRowValue = this.rowPreviousValue;
const updateData = {
previousRowValue,
newRowValue
};
this.subscriptions.push(
this.sqlService.updateTableData(updateData)
.subscribe((resp) => {
if (resp) {
this.snackBar.openFromComponent(ConfirmComponent, {
duration: 3000,
data: `${this.rowsAffected} row(s) updated.`
});
}
}, (err) => {
this.snackBar.openFromComponent(ErrorComponent, {
duration: 6000,
data: `${err}`
});
})
);
}
public deleteRow(item: object): void {
const tableData = {
row: item,
currentTableData: this.currentTableData
};
this.subscriptions.push(
this.sqlService.deleteTableData(tableData)
.subscribe((resp) => {
if (resp) {
this.loadedTableData = this.loadedTableData.filter(obj => obj !== item);
this.snackBar.openFromComponent(ErrorComponent, {
duration: 3000,
data: `${this.rowsAffected} row(s) deleted.`
});
}
}, (err) => {
this.snackBar.openFromComponent(ErrorComponent, {
duration: 6000,
data: `${err}`
});
})
);
}
public ngOnDestroy(): void {
for (const sub of this.subscriptions) {
sub.unsubscribe();
}
}
}
sql.service.ts:
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { tap, catchError, delay } from 'rxjs/operators';
import { Observable, BehaviorSubject, of, throwError } from 'rxjs';
import { ITableList } from '../interfaces/ITableList.interface';
import { MatSnackBar } from '@angular/material';
import { ErrorComponent } from '../snackbar/error/error.component';
@Injectable({
providedIn: 'root',
})
export class SqlService {
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { tap, catchError, delay } from 'rxjs/operators';
import { Observable, BehaviorSubject, of, throwError } from 'rxjs';
import { ITableList } from '../interfaces/ITableList.interface';
import { MatSnackBar } from '@angular/material';
import { ErrorComponent } from '../snackbar/error/error.component';
@Injectable({
providedIn: 'root',
})
export class SqlService {
private uri = 'http://localhost:8080';
private headers = new HttpHeaders({ 'Content-Type': 'application/json; charset=utf-8' });
public currentDataView: BehaviorSubject<any> = new BehaviorSubject<any>([]);
public currentTableData: BehaviorSubject<any> = new BehaviorSubject<any>({});
public tableHasBeenLoaded: BehaviorSubject<any> = new BehaviorSubject<boolean>(false);
public rowsAffected: BehaviorSubject<number> = new BehaviorSubject<number>(0);
constructor(private http: HttpClient,
private snackBar: MatSnackBar) { }
public getMasterDBList(): Observable<any> {
return this.http.get<ITableList>(`${this.uri}/api/masterDBList`)
.pipe(
tap(
response => {
console.log(response);
}
)
);
}
public deleteTableData(tableRow: any): Observable<any> {
const parsedData = JSON.parse(JSON.stringify(tableRow));
if (tableRow) {
return this.http.post<any>(`${this.uri}/api/deleteTableData`, parsedData).pipe(
tap(
response => {
this.rowsAffected.next(response.rowsAffected);
}
),
catchError(this.handleError)
);
}
}
public insertTableData(tableData: any): Observable<any> {
const parsedData = JSON.parse(JSON.stringify(tableData));
if (tableData.row) {
return this.http.post<any>(`${this.uri}/api/insertTableData`, parsedData).pipe(
tap(
response => {
this.rowsAffected.next(response.rowsAffected);
}
),
catchError(this.handleError)
);
}
}
public updateTableData(updateData: any): Observable<any> {
const parsedUpdateData = JSON.parse(JSON.stringify(updateData));
const parsedData = {
currentTableData: this.currentTableData.getValue(),
parsedUpdateData
};
if (updateData) {
return this.http.post<any>(`${this.uri}/api/updateTableData`, parsedData).pipe(
tap(
response => {
this.rowsAffected.next(response.rowsAffected);
}
),
catchError(this.handleError)
);
}
}
public getTableData(tableData?: any): Observable<any> {
// clear currentDataView so that load icon appears between queries.
this.currentDataView.next([]);
// for state purposes, subscribers are notified that a GET has been called in this session.
this.tableHasBeenLoaded.next(true);
const parsedData = JSON.parse(JSON.stringify(tableData));
return this.http.get<object[]>(`${this.uri}/api/getTableData`, {
params: parsedData,
headers: this.headers
})
.pipe(
tap(
response => {
this.currentDataView.next(response);
console.log(this.currentDataView.getValue());
}
),
catchError(this.handleError)
);
}
public handleError(errorResponse: HttpErrorResponse): Observable<any> {
if (errorResponse.error instanceof ErrorEvent) {
console.error('Client Side Error: ', errorResponse.error.message);
} else {
console.error('Server Side Error: ', errorResponse);
}
return throwError(errorResponse.error.message);
}
}
PAGE 1 results:
PAGE 2 results:
Upvotes: 1
Views: 6301
Reputation: 431
So, I figured out what happened. In my ngFor loop I was binding the models based on its index in an array. Problem is, that the index will always be 0-9 since that is what Is being rendered in the template.
Meaning my ngModels will always use myPropertyName_0 - myPropertyName_9 every time a new page is viewed, binding the same 10 models over and over for each page rendered.
The fix was simple, instead of using the index I used the local variable I created 'item' from the ngFor loop in the parent element to access the REAL model for that row's data.
Old implementation:
<tbody>
<tr *ngFor="let item of loadedTableData
| paginate: {
id: 'ltd',
itemsPerPage: 10,
currentPage: page
};
let i= index;"
(click)="updatePreviousValue(item);">
<td class="form-group" #rowIndex>
<span> {{ i + 1 }} </span>
</td>
<td *ngFor="let property of item | keys;"
class="form-group" #editRow>
<input #editRowProp
<!-- WRONG MODEL BEING BOUND -->
**[(ngModel)]="loadedTableData[i][property]"**
class="form-control"
[name]="property + '_' + i"
type="text">
</td>
<td>
<button type="button" class="btn btn-primary
rounded
btn-sm my-0"
(click)="updateRow(loadedTableData[i])">Update</button>
<hr>
<button type="button" class="btn btn-danger
rounded
btn-sm my-0" (click)="deleteRow(item)">Remove</button>
</td>
</tr>
</tbody>
Fixed code:
<tbody>
<tr *ngFor="let item of loadedTableData
| paginate: {
id: 'ltd',
itemsPerPage: 10,
currentPage: page
};
let i= index;"
(click)="updatePreviousValue(item);">
<td class="form-group" #rowIndex>
<span> {{ i + 1 }} </span>
</td>
<td *ngFor="let property of item | keys;"
class="form-group" #editRow>
<input #editRowProp
<!-- CORRECT MODEL ! -->
**[(ngModel)]="item[property]"**
class="form-control"
[name]="property + '_' + i"
type="text">
</td>
<td>
<button type="button" class="btn btn-primary
rounded
btn-sm my-0"
(click)="updateRow(loadedTableData[i])">Update</button>
<hr>
<button type="button" class="btn btn-danger
rounded
btn-sm my-0" (click)="deleteRow(item)">Remove</button>
</td>
</tr>
</tbody>
Upvotes: 1