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).
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;
}
来源:https://stackoverflow.com/questions/46433195/angular-use-modal-dialog-in-candeactivate-guard-service-for-unsubmitted-changes