Angular - Trigger ngOnChanges in a Child Component for Array or Object
Tutorial built with Angular 14.2.12
This is a quick post to show to trigger the Angular OnChanges lifecycle hook in a child component for a data-bound array or object.
The example code is from a Angular paging and sorting tutorial I posted recently which triggers the ngOnChanges()
method in a child <pagination>
component to reset to the first page when table data is sorted. For the full paging/sorting tutorial see Angular 14 - Paging and Sorting Table Data Example & Tutorial.
Angular OnChanges
The Angular OnChanges hook is triggered when any data-bound property (e.g. [someProp]="someValue"
) of a component changes.
Changes not detected on array or object
There's a catch, when a data-bound property points to an array or object Angular change detection only checks if the reference has changed, so modifying the contents of the array or properties of the object will not trigger ngOnChanges()
.
I ran into this issue because the Array sort()
method updates the array in-place meaning the reference still points to the same array, so changes aren't detected by Angular.
Trigger ngOnChanges for Array or Object
The solution turned out to be pretty simple, copy the contents into a new array or object with the javascript spread operator (...
). This creates a new reference which triggers Angular change detection to call ngOnChanges()
.
Here's how to copy an Angular property of type array or object with the spread operator:
// copy array prop to trigger change detection
this.myArr = [...this.myArr];
// copy object prop to trigger change detection
this.myObj = {...this.myObj};
Pagination component with ngOnChanges()
method
This is the example pagination component mentioned above that implements the Angular OnChanges
interface. The ngOnChanges()
method sets the component to the initial page by calling this.setPage(this.initialPage)
when the items
array is changed (including when items are first loaded).
The setPage()
method sets which page of items to display. It executes the paginate()
method to get the pager properties for the specified page, slices out the current page of items into a new array (pageOfItems
), then emits the changePage
event to the parent component with the current page of items.
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
@Component({ selector: 'pagination', templateUrl: 'pagination.component.html' })
export class PaginationComponent implements OnChanges {
@Input() items?: Array<any>;
@Output() changePage = new EventEmitter<any>(true);
@Input() initialPage = 1;
@Input() pageSize = 10;
@Input() maxPages = 10;
pager?: Pager;
ngOnChanges(changes: SimpleChanges) {
// set page when items array first set or changed
if (changes.items.currentValue !== changes.items.previousValue) {
this.setPage(this.initialPage);
}
}
setPage(page: number) {
if (!this.items?.length)
return;
// get new pager object for specified page
this.pager = this.paginate(this.items.length, page, this.pageSize, this.maxPages);
// get new page of items from items array
const pageOfItems = this.items.slice(this.pager.startIndex, this.pager.endIndex + 1);
// call change page function in parent component
this.changePage.emit(pageOfItems);
}
paginate(totalItems: number, currentPage: number = 1, pageSize: number = 10, maxPages: number = 10): Pager {
// calculate total pages
let totalPages = Math.ceil(totalItems / pageSize);
// ensure current page isn't out of range
if (currentPage < 1) {
currentPage = 1;
} else if (currentPage > totalPages) {
currentPage = totalPages;
}
let startPage: number, endPage: number;
if (totalPages <= maxPages) {
// total pages less than max so show all pages
startPage = 1;
endPage = totalPages;
} else {
// total pages more than max so calculate start and end pages
let maxPagesBeforeCurrentPage = Math.floor(maxPages / 2);
let maxPagesAfterCurrentPage = Math.ceil(maxPages / 2) - 1;
if (currentPage <= maxPagesBeforeCurrentPage) {
// current page near the start
startPage = 1;
endPage = maxPages;
} else if (currentPage + maxPagesAfterCurrentPage >= totalPages) {
// current page near the end
startPage = totalPages - maxPages + 1;
endPage = totalPages;
} else {
// current page somewhere in the middle
startPage = currentPage - maxPagesBeforeCurrentPage;
endPage = currentPage + maxPagesAfterCurrentPage;
}
}
// calculate start and end item indexes
let startIndex = (currentPage - 1) * pageSize;
let endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1);
// create an array of pages to ng-repeat in the pager control
let pages = Array.from(Array((endPage + 1) - startPage).keys()).map(i => startPage + i);
// return object with all pager properties required by the view
return {
totalItems,
currentPage,
pageSize,
totalPages,
startPage,
endPage,
startIndex,
endIndex,
pages
};
}
}
export interface Pager {
totalItems: number;
currentPage: number;
pageSize: number;
totalPages: number;
startPage: number;
endPage: number;
startIndex: number;
endIndex: number;
pages: number[];
}
App component template with a <pagination>
child component
The app component template contains a pageable and sortable table of data. Sorting is triggered by clicking any of the table header columns which are bound to the sortBy()
method.
Pagination logic and controls are encapsulated in the <pagination>
component. The items
array of the app component is bound to the items
property of the pagination component with the Angular data binding attribute [items]="items"
. The onChangePage()
method of the app component is bound to the changePage
event of the pagination component with the Angular event binding attribute (changePage)="onChangePage($event)"
. The $event
argument is the current page of items (pageOfItems
) when the event is emitted from the setPage()
method of the pagination component.
<!-- main app container -->
<div class="card m-3">
<h3 class="card-header text-center">Angular 14 - Paging and Sorting Table Data Example</h3>
<div class="card-body">
<table class="table table-striped table-sm">
<thead>
<tr>
<th><a (click)="sortBy('id')">Id {{sortIcon('id')}}</a></th>
<th><a (click)="sortBy('firstName')">First Name {{sortIcon('firstName')}}</a></th>
<th><a (click)="sortBy('lastName')">Last Name {{sortIcon('lastName')}}</a></th>
<th><a (click)="sortBy('age')">Age {{sortIcon('age')}}</a></th>
<th><a (click)="sortBy('company')">Company {{sortIcon('company')}}</a></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of pageOfItems">
<td>{{item.id}}</td>
<td>{{item.firstName}}</td>
<td>{{item.lastName}}</td>
<td>{{item.age}}</td>
<td>{{item.company}}</td>
</tr>
<tr *ngIf="loading">
<td colspan="5" class="text-center py-5">
<span class="spinner-border spinner-border-lg align-center"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer pb-0 pt-3">
<pagination [items]="items" (changePage)="onChangePage($event)"></pagination>
</div>
</div>
App component that triggers ngOnChanges()
in child component
The app component contains the logic for the pageable sortable table of data.
You can see the javascript spread operator (...
) being used by the sortBy()
method to copy the result of this.items.sort()
into a new array that is assigned to this.items
. As a result Angular will detect the change to the data-bound items
array and call ngOnChanges()
in the child pagination component.
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({ selector: 'app-root', templateUrl: './app.component.html' })
export class AppComponent {
items: any[] = [];
pageOfItems?: Array<any>;
sortProperty: string = 'id';
sortOrder = 1;
loading = false;
constructor(private http: HttpClient) { }
ngOnInit() {
// fetch items from the backend api
this.loading = true;
this.http.get<any[]>('/items')
.subscribe(x => {
this.items = x;
this.loading = false;
});
}
onChangePage(pageOfItems: Array<any>) {
// update current page of items
this.pageOfItems = pageOfItems;
}
sortBy(property: string) {
this.sortOrder = property === this.sortProperty ? (this.sortOrder * -1) : 1;
this.sortProperty = property;
this.items = [...this.items.sort((a: any, b: any) => {
// sort comparison function
let result = 0;
if (a[property] < b[property]) {
result = -1;
}
if (a[property] > b[property]) {
result = 1;
}
return result * this.sortOrder;
})];
}
sortIcon(property: string) {
if (property === this.sortProperty) {
return this.sortOrder === 1 ? '☝️' : '👇';
}
return '';
}
}
Need Some Angular Help?
Search fiverr for freelance Angular developers.
Follow me for updates
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!