Angular2 routing issue and ngOnInit called twice
Asked Answered
S

2

14

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

Spiv answered 9/3, 2017 at 16:8 Comment(7)
Are you using the most recent Angular2 version? There was an issue a while ago that caused this behavior but was fixed a while ago AFAIR.Knowledgeable
Try removing the wildcard route and see if the issue goes away. I remember reading something about wildcard routes needing to be last in the list, and I'm not so sure that is the case in your configuration.Spaniel
I'm using 2.4.8, and @angular/router version 3.4.8. And the issue still occurs, even without the wildcard route.Spiv
Even if I remove the empty route as well, the issue still persists.Spiv
Could you please provide notification-list.component.html code?Grained
@Grained Added it under notification-list.component.tsSpiv
Ok Html looks good. Lets find out how navigation is flowing. we need to register various route change / start / end event which will give us idea and may be the reason about what was the url before navigation. try this one #33520543Grained
S
7

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.

Spiv answered 15/3, 2017 at 12:46 Comment(2)
Free tip, you can also use the elvis operator #34833858Concavoconvex
Official docs here: angular.io/docs/ts/latest/guide/…Concavoconvex
B
0

My guess would be that your browser is interpreting your edit link as an actual link instead of doing what you want it to do. This is based on the fact that you see a flash - sounds like the browser is trying to visit the link in question.

Try changing the item that you click on in your notification list to edit the notification from an anchor tag to a simple span or div. If you don't get the flash, then you know that was your issue.

Betteanne answered 13/3, 2017 at 18:47 Comment(1)
Nope, changing to a div didn't work. And it's not the actual page that flashes. The page actually renders the edit template correctly (other than the model not binding to inputs correctly), it's only the address bar that flashes to the correct edit route, then back to the list route, while the edit template remains displayed on the actual page.Spiv

© 2022 - 2024 — McMap. All rights reserved.