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?
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">×</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).
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;
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With