Angular use modal dialog in canDeactivate Guard service for unsubmitted changes (Form dirty)

寵の児 提交于 2019-11-27 21:37:52
Francesco Borzi

I solved it using ngx-bootstrap Modals and RxJs Subjects.

First of all I created a Modal Component:

import { Component } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { BsModalRef } from 'ngx-bootstrap';

@Component({
  selector: 'app-confirm-leave',
  templateUrl: './confirm-leave.component.html',
  styleUrls: ['./confirm-leave.component.scss']
})
export class ConfirmLeaveComponent {

  subject: Subject<boolean>;

  constructor(public bsModalRef: BsModalRef) { }

  action(value: boolean) {
    this.bsModalRef.hide();
    this.subject.next(value);
    this.subject.complete();
  }
}

here's the template:

<div class="modal-header modal-block-primary">
  <button type="button" class="close" (click)="bsModalRef.hide()">
    <span aria-hidden="true">&times;</span><span class="sr-only">Close</span>
  </button>
  <h4 class="modal-title">Are you sure?</h4>
</div>
<div class="modal-body clearfix">
  <div class="modal-icon">
    <i class="fa fa-question-circle"></i>
  </div>
  <div class="modal-text">
    <p>The form has not been submitted yet, do you really want to leave page?</p>
  </div>
</div>
<div class="modal-footer">
  <button class="btn btn-default" (click)="action(false)">No</button>
  <button class="btn btn-primary right" (click)="action(true)">Yes</button>
</div>

Then I modified my guard using a Subject, now it look like this:

import { CanDeactivate } from '@angular/router';
import { FormGroup } from '@angular/forms';
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { BsModalService } from 'ngx-bootstrap';

import { ConfirmLeaveComponent } from '.....';

export interface FormComponent {
  form: FormGroup;
}

@Injectable()
export class UnsavedChangesGuardService implements CanDeactivate<FormComponent> {

  constructor(private modalService: BsModalService) {}

  canDeactivate(component: FormComponent) {
    if (component.form.dirty) {
      const subject = new Subject<boolean>();

      const modal = this.modalService.show(ConfirmLeaveComponent, {'class': 'modal-dialog-primary'});
      modal.content.subject = subject;

      return subject.asObservable();
    }

    return true;
  }
}

In app.module.ts file go to the @NgModule section and add the ConfirmLeaveComponent component to entryComponents.

@NgModule({
  entryComponents: [
    ConfirmLeaveComponent,
  ]
})

In addition to ShinDarth's good solution, it seems worth mentioning that you will have to cover a dismissal of the modal as well, because the action() method might not be fired (e.g. if you allow the esc button or click outside of the modal). In that case the observable never completes and your app might get stuck if you use it for routing.

I achieved that by subscribing to the bsModalService onHide property and merging this and the action subject together:

confirmModal(text?: string): Observable<boolean> {
    const subject = new Subject<boolean>();
    const modal = this.modalService.show(ConfirmLeaveModalComponent);
    modal.content.subject = subject;
    modal.content.text = text ? text : 'Are you sure?';
    const onHideObservable = this.modalService.onHide.map(() => false);
    return merge(
      subject.asObservable(),
      onHideObservable
    );
  }

In my case I map the mentioned onHide observable to false because a dismissal is considered an abort in my case (only a 'yes' click will yield a positive outcome for my confirmation modal).

Since I have been going back and forth with a Ashwin, I decided to post my solution that i have with Angular and Material.

Here is my StackBlitz

This works, but I wanted add the complexity of an asynchronous response from the Deactivating page like how I have it in my application. This is bit of a process so bear with me please.

This is my implementation to get a confirmation dialog before leaving a certain route using ngx-bootstrap dialog box. I am having a global variable called 'canNavigate' with the help of a service. This variable will hold a Boolean value if it is true or false to see if navigation is possible. This value is initially true but if I do a change in my component I will make it false therefore 'canNavigate' will be false. If it is false I will open the dialog box and if the user discards the changes it will go to the desired route by taking the queryParams as well, else it will not route.

@Injectable()
export class AddItemsAuthenticate implements CanDeactivate<AddUniformItemComponent> {

  bsModalRef: BsModalRef;
  constructor(private router: Router,
              private dataHelper: DataHelperService,
              private modalService: BsModalService) {
  }

  canDeactivate(component: AddUniformItemComponent,
                route: ActivatedRouteSnapshot,
                state: RouterStateSnapshot,
                nextState?: RouterStateSnapshot): boolean {
    if (this.dataHelper.canNavigate === false ) {
      this.bsModalRef = this.modalService.show(ConfirmDialogComponent);
      this.bsModalRef.content.title = 'Discard Changes';
      this.bsModalRef.content.description = `You have unsaved changes. Do you want to leave this page and discard
                                            your changes or stay on this page?`;

      this.modalService.onHidden.subscribe(
        result => {
          try {
            if (this.bsModalRef && this.bsModalRef.content.confirmation) {
              this.dataHelper.canNavigate = true;
              this.dataHelper.reset();;
              const queryParams = nextState.root.queryParams;
              this.router.navigate([nextState.url.split('?')[0]],
                {
                  queryParams
                });
            }
          }catch (exception) {
            // console.log(exception);
          }
        }, error => console.log(error));
    }

    return this.dataHelper.canNavigate;

  }
}

I implemented this solution with Angular Material Dialog:

Material's modal has "componentInstance" instead of "content" in ngx-bootstrap Modals:

if (component.isDirty()) {
  const subject = new Subject<boolean>();
  const modal = this.dialog.open(ConfirmationDialogComponent, {
    panelClass: 'my-panel', width: '400px', height: '400px',
  });

  modal.componentInstance.subject = subject;
  return subject.asObservable()
}
  return true;
}

Just expanding on the additional info provided by mitschmidt regarding click outside / escape button, this canDeactivate method works with Francesco Borzi's code. I just add the subscribe to onHide() inline in the function:

canDeactivate(component: FormComponent) {
        if (component.form.dirty) {
            const subject = new Subject<boolean>();

            const modal = this.modalService.show(ConfirmLeaveComponent, { 'class': 'modal-dialog-primary' });
            modal.content.subject = subject;

            this.modalService.onHide.subscribe(hide => {
                subject.next(false);
                return subject.asObservable();
            });

            return subject.asObservable();
        }

        return true;
    }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!