Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular2 routing issue and ngOnInit called twice

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()">&times;</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()">&times;</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?

Update:

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.

Update 2:

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:

Route logging

like image 683
Andrew Mairose Avatar asked Mar 09 '17 16:03

Andrew Mairose


1 Answers

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:

enter image description here

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.

like image 54
Andrew Mairose Avatar answered Sep 27 '22 16:09

Andrew Mairose