Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

In my Angular 4 application I have some components with a form, like this:

export class MyComponent implements OnInit, FormComponent {

  form: FormGroup;

  ngOnInit() {
    this.form = new FormGroup({...});
  }

they use a Guard service to prevent unsubmitted changes to get lost, so if the user tries to change route before it will ask for a confirmation:

import { CanDeactivate } from '@angular/router';
import { FormGroup } from '@angular/forms';

export interface FormComponent {
  form: FormGroup;
}

export class UnsavedChangesGuardService implements CanDeactivate<FormComponent> {
  canDeactivate(component: FormComponent) {
    if (component.form.dirty) {
      return confirm(
        'The form has not been submitted yet, do you really want to leave page?'
      );
    }

    return true;
  }
}

This is using a simple confirm(...) dialog and it works just fine.

However I would like to replace this simple dialog with a more fancy modal dialog, for example using the ngx-bootstrap Modal.

How can I achieve the same result using a modal instead?

like image 937
Francesco Borzi Avatar asked Sep 26 '17 18:09

Francesco Borzi


4 Answers

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,
  ]
})
like image 107
Francesco Borzi Avatar answered Nov 09 '22 21:11

Francesco Borzi


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).

like image 37
mitschmidt Avatar answered Nov 09 '22 20:11

mitschmidt


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;
    }
like image 3
Cameron Forward Avatar answered Nov 09 '22 22:11

Cameron Forward


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.

like image 2
Andrew Lobban Avatar answered Nov 09 '22 20:11

Andrew Lobban