I'm having a very odd issue with Angular 2 routing to where my ngOnInit
in the component that I am routing to is getting called twice, and the route in the browser is getting reset to the original route.
I have a NotificationListComponent
and a NotificationEditComponent
in a MaintenanceModule
.
In my root AppModule
, I setup the RouterModule
to redirect any unmapped routes to /maintenance/list
.
app.module.ts:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpModule,
RouterModule.forRoot([
{path: "", redirectTo: "maintenance/list", pathMatch: "full"},
{path: "**", redirectTo: "maintenance/list", pathMatch: "full"}
], {useHash: true}),
CoreModule.forRoot({notificationUrl: "http://localhost:8080/notification-service/notifications"}),
MaintenanceModule
],
providers: [NotificationService],
bootstrap: [AppComponent]
})
export class AppModule { }
And I have the /maintenance/list
route defined in my MaintenanceModule
, which points at my NotificationListComponent
, as well as a /maintenance/edit/:id
route which points at my NotificationEditComponent
.
maintenance.module.ts:
@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{path: "maintenance/list", component: NotificationListComponent, pathMatch: 'full'},
{path: "maintenance/edit/:id", component: NotificationEditComponent, pathMatch: 'full'}
]),
FormsModule
],
declarations: [
NotificationListComponent,
NotificationEditComponent
]
})
export class MaintenanceModule {}
When my application loads, it correctly follows the /maintenance/list
route, and I can see all of my notifications in a list. For each notification in the list there is an edit icon, which has its click
event bound to the edit(id: number)
method in my NotificationListComponent
notification-list.component.ts:
@Component({
templateUrl: 'notification-list.component.html'
})
export class NotificationListComponent implements OnInit {
notifications: Notification[];
errorMessage: string;
constructor(private _notificationService: NotificationService,
private _router: Router) {}
ngOnInit(): void {
this._notificationService.getNotifications()
.subscribe(
notifications => this.notifications = notifications,
error => this.errorMessage = <any>error);
}
clearError(): void {
this.errorMessage = null;
}
}
notification-list.component.html:
<div class="row">
<h1>Notification Maintenance</h1>
<div *ngIf="errorMessage" class="alert-box alert">
<span>{{errorMessage}}</span>
<a class="right" (click)="clearError()">×</a>
</div>
<p-dataTable [value]="notifications" [sortField]="'code'" [responsive]="true" [sortOrder]="1" [rows]="10" [paginator]="true" [rowsPerPageOptions]="[10,50,100]">
<p-header>Available Notifications</p-header>
<p-column [field]="'code'" [header]="'Code'" [sortable]="true" [style]="{'width':'10%'}"></p-column>
<p-column [field]="'name'" [header]="'Name'" [sortable]="true" [style]="{'width':'40%'}"></p-column>
<p-column [field]="'roles'" [header]="'Roles'" [style]="{'width':'40%'}"></p-column>
<p-column [field]="'notificationId'" [header]="'Edit'" [style]="{'width':'10%'}">
<template let-row="rowData" pTemplate="body">
<a [routerLink]="'/maintenance/edit/' + row['notificationId']"><span class="fa fa-pencil fa-2x"></span></a>
</template>
</p-column>
</p-dataTable>
</div>
As you can see, the edit(id: number)
method should navigate to the /maintenance/edit/:id
route. When I click the icon to navigate to that route, the browser flashes the correct route in the address bar (e.g. localhost:4200/#/maintenance/edit/2
), but then the route in the address bar immediately changes back to localhost:4200/#/maintenance/list
. Even though the route returned to /maintenance/list
in the address bar, my NotificationEditComponent
is still visible in the actual application. However, I can see that the ngOnInit
method is being called twice in my NotificationEditComponent
, because the id
gets logged to the console twice, and if I put a breakpoint in the ngOnInit
function, it hits that breakpoint twice.
notification-edit.component.ts:
@Component({
templateUrl: "notification-edit.component.html"
})
export class NotificationEditComponent implements OnInit{
notification: Notification;
errorMessage: string;
constructor(private _notificationService: NotificationService,
private _route: ActivatedRoute,
private _router: Router) {
}
ngOnInit(): void {
let id = +this._route.snapshot.params['id'];
console.log(id);
this._notificationService.getNotification(id)
.subscribe(
notification => this.notification = notification,
error => this.errorMessage = <any>error
);
}
}
This appears to also be causing other issues, because when attempting to bind input
values to values in my NotificationEditComponent
using, for example [(ngModel)]="notification.notificationId"
, the value is not being displayed on the screen, even though I can see with the Augury chrome extension, as well as logging the object to the console, that the value is populated in the component.
notification-edit.component.html:
<div class="row">
<h1>Notification Maintenance</h1>
<div *ngIf="errorMessage" class="alert-box alert">
<span>{{errorMessage}}</span>
<a class="right" (click)="clearError()">×</a>
</div>
<p-fieldset [legend]="'Edit Notification'">
<label for="notificationId">ID:
<input id="notificationId" type="number" disabled [(ngModel)]="notification.notificationId"/>
</label>
</p-fieldset>
</div>
Does anyone have any idea why this would be happening?
I removed my calls to the NotificationService
, and replaced them with just some mock data, and then the routing started working! But as soon as I add calls to my service, I get the same issue I described above. I even removed the CoreModule
and just added the service directly to my MaintenanceModule
, and still got the same issue whenever I use the actual service instead of just mock data.
notification.service.ts:
@Injectable()
export class NotificationService {
private _notificationUrl : string = environment.servicePath;
constructor(private _http: Http) {
}
getNotifications(): Observable<Notification[]> {
return this._http.get(this._notificationUrl)
.map((response: Response) => <Notification[]>response.json())
.catch(this.handleGetError);
}
getNotification(id: number): Observable<Notification> {
return this._http.get(this._notificationUrl + "/" + id)
.map((response: Response) => <Notification>response.json())
.catch(this.handleGetError);
}
postNotification(notification: Notification): Observable<number> {
let id = notification.notificationId;
let requestUrl = this._notificationUrl + (id ? "/" + id : "");
return this._http.post(requestUrl, notification)
.map((response: Response) => <number>response.json())
.catch(this.handlePostError);
}
private handleGetError(error: Response) {
console.error(error);
return Observable.throw('Error retrieving existing notification(s)!');
}
private handlePostError(error: Response) {
console.error(error);
return Observable.throw('Error while attempting to save notification!');
}
}
And the service seems to run fine - I can see that the endpoint successfully returns data and I can see that the data looks correct when I look at my NotificationEditComponent
with the Augury chrome extension. But the data does not show in the template, and the route in the URL returns to /maintenance/list
even though the template for the /maintenance/edit/:id
route is still displayed.
As suggested by @user3249448, I added the following to my AppComponent
for some debugging:
constructor(private _router: Router) {
this._router.events.pairwise().subscribe((event) => {
console.log(event);
});
}
Here is the output of that when I click on one of the "edit" links:
Was finally able to resolve the issue after getting the debugging help from @user3249448.
Turns out I was getting this NavigationError
, even though no errors were being logged to the console:
Here was the full stack trace:
TypeError: Cannot read property 'notificationId' of undefined
at CompiledTemplate.proxyViewClass.View_NotificationEditComponent0.detectChangesInternal (/MaintenanceModule/NotificationEditComponent/component.ngfactory.js:487:49)
at CompiledTemplate.proxyViewClass.AppView.detectChanges (http://localhost:4200/vendor.bundle.js:80125:14)
at CompiledTemplate.proxyViewClass.DebugAppView.detectChanges (http://localhost:4200/vendor.bundle.js:80320:44)
at CompiledTemplate.proxyViewClass.AppView.internalDetectChanges (http://localhost:4200/vendor.bundle.js:80110:18)
at CompiledTemplate.proxyViewClass.View_NotificationEditComponent_Host0.detectChangesInternal (/MaintenanceModule/NotificationEditComponent/host.ngfactory.js:29:19)
at CompiledTemplate.proxyViewClass.AppView.detectChanges (http://localhost:4200/vendor.bundle.js:80125:14)
at CompiledTemplate.proxyViewClass.DebugAppView.detectChanges (http://localhost:4200/vendor.bundle.js:80320:44)
at ViewRef_.detectChanges (http://localhost:4200/vendor.bundle.js:60319:20)
at RouterOutlet.activate (http://localhost:4200/vendor.bundle.js:65886:42)
at ActivateRoutes.placeComponentIntoOutlet (http://localhost:4200/vendor.bundle.js:24246:16)
at ActivateRoutes.activateRoutes (http://localhost:4200/vendor.bundle.js:24213:26)
at http://localhost:4200/vendor.bundle.js:24149:58
at Array.forEach (native)
at ActivateRoutes.activateChildRoutes (http://localhost:4200/vendor.bundle.js:24149:29)
at ActivateRoutes.activate (http://localhost:4200/vendor.bundle.js:24123:14)
So essentially, my template was being rendered before the call to my web service was returned, and my template could not be rendered properly because notification
was undefined
, so I got this NavigationError
, which caused the described behavior (doesn't it seem like this error should be logged to the console without having to add extra debugging code in your AppComponent
?).
To fix this, all I had to do was add an *ngIf
to my fieldset
that contained all the information about the notification
.
<p-fieldset [legend]="'Edit Notification'" *ngIf="notification">
And now my template loads properly once the data is returned from the web service.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With