1

I read about using CanDeactivate to detect whether a user leaves a form/route and this is what I need in my project. I tried to implement this SO answer: SO Answer

When I view my user-form component, I enter information into one of the inputs and then try to refresh, clicking on another component, navigating to google.com via browser url. None of these triggers the confirm prompt in that answer.

My project structure is setup like this:

> src
 > app
  - app-routing.module.ts
  - app.module.ts
  - app.component.ts
  - app.component.html
  > auth
   > ...
  > dashboard
   - dashboard.module.ts
   - dashboard.component.ts
   - dashboard.component.html
   > components
    > forms
     - user-form.component.ts
     - user-form.component.html
    > ...
  > shared
   > guards
    - pending-changes.guard.ts
   > ...

I have already tried implementing this SO answer: SO Answer

In my dashboard Module, I imported my pending-changes.guard and have one route defined for my dashboard component which uses my canDeactivate: [PendingChangesGuard] Then I add PendingChangesGuard as a provider in my NgModule for my dashboard module.

In my user-form component, I import ComponentCanDeactivate from my PendingChangesGuard in shared and then implement the ComponentCanDeactivate interface in the component. I define a function called canDeactivate() that checks whether my userForm of type FormGroup is dirty and if it's dirty then I return false which should show the confirmation to the user. If the form hasn't been used then I want to return true.

My route defined in my dashboard module is for my dashboard component. My dashboard component is a parent of another component to my user-form component. So my user-form component that contains the html for my form does not specifically have its own url/path/route/routerLink set up for when a user displays that component by clicking other components. I'm not sure if this setup here is correct because most examples I see have a route/path/routerLink specifically set up for the form component that would contain the unsaved data that would trigger a user prompt.

pending-changes.guard.ts

import { CanDeactivate } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

export interface ComponentCanDeactivate {
  canDeactivate: () => boolean | Observable<boolean>;
}

@Injectable()
export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> {
  canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
    // if there are no pending changes, just allow deactivation; else confirm first
    return component.canDeactivate() ?
      true :
      // NOTE: this warning message will only be shown when navigating elsewhere within your angular app;
      // when navigating away from your angular app, the browser will show a generic warning message
      // see http://stackoverflow.com/a/42207299/7307355
      confirm('WARNING: You have unsaved changes. Press Cancel to go back and save these changes, or OK to lose these changes.');
  }
}

dashboard.module.ts

import { PendingChangesGuard } from '../shared/guards/pending-changes.guard';

export const routes: Routes = [
  {
    path: '',
    component: DashboardComponent,
    canDeactivate: [PendingChangesGuard]
  }
];

@NgModule({
  imports: [
    CodemirrorModule,
    CommonModule,
    CronEditorModule,
    FormsModule,
    ReactiveFormsModule,
    SimpleNotificationsModule.forRoot(),
    NgbModule,
    NgxsModule.forFeature(STATES),
    NgxSpinnerModule,
    RouterModule.forChild(routes),
    SharedModule
  ],
  declarations: COMPONENTS,
  exports: COMPONENTS,
  entryComponents: [ProfileComponent],
  providers: [PendingChangesGuard]
})
export class DashboardModule {}

user-form.component.ts

import { ComponentCanDeactivate } from '../../../../shared/guards/pending-changes.guard';
@Component({
  selector: 'user-form',
  templateUrl: './user-form.component.html',
  styleUrls: ['./user-form.component.scss']
})
export class UserFormComponent implements OnInit, ComponentCanDeactivate {
  ...

  userForm: FormGroup;

  constructor(private fb: FormBuilder) {}

  // @HostListener allows us to also guard against browser refresh, close, etc.
  @HostListener('window:beforeunload')
  canDeactivate(): Observable<boolean> | boolean {
    // insert logic to check if there are pending changes here;
    if (this.userForm.dirty === true) return false;
    return true;
    // returning true will navigate without confirmation
    // returning false will show a confirm dialog before navigating away
  }

  ...
}

When I view my user-form component, I enter information into one of the inputs and then try to refresh, clicking on another component, navigating to google.com via browser url. None of this causes the confirm prompt in my PendingChangesGuard. I'm expecting the user to be prompted if he has unsaved data in the user form component.

UPDATE: I have a confirmation popping up for Refresh and Browser URL navigation but this doesn't cover all of my needs, I think this functionality is coming mainly from the @HostListener. It doesn't seem to be the exact message I thought I was sending in my Guard though. I read somewhere that browsers handle these types of confirmations on their own so I'm not too surprised but maybe this means I should be handling this with my own custom modal? I'm using Chrome Version 70.0.3538.77 (Official Build) (64-bit) to test my UI on.

Some other problems I'm thinking about is that I have this big component made up of other components. The problem is me and the original project owner(I'm new to the project and now he is not with the company anymore and I'm still picking up angular) never thought of routing components past the dashboard component because we handled what to display in each component through NGXS state management. Usually the top level components(The dashboard navigation tabs) have @inputs and @outputs to allow data to flow down/up through the components from the state which the state gets its data from NGXS Actions that make API calls and place the data in a state model that makes sense for display in the dashboard component.

And because I don't have routing for each component, if the user clicks a button that causes another component to display instead of my user-form component via an ngSwitch, then I don't receive the confirmation of unsaved data in my user-form component because I'm not technically "Navigating" or changing the route.

So how should I organize this project? Do I add routes down to each component in my Dashboard module routes even if my Dashboard module has lazy-loaded children from app-routing.module.ts? Or do I have to build hooks into everything clickable when in the form to check the form's dirty value? Or is there an easier way to handle this from the user-form component like using a lifecycle hook(OnDestroy() comes to mind but I think this is too late in the lifecycle to handle this problem)?

app-routing.module.ts has these routes

const routes: Routes = [
  {
    path: 'dashboard',
    canActivate: [AuthGuard],
    loadChildren: './dashboard/dashboard.module#DashboardModule'
  },
  {
    path: '',
    redirectTo: '/dashboard',
    pathMatch: 'full'
  }
];

0 Answers0