Модальное диалоговое окно углового использования в сервисе canDeactivate Guard для неподтвержденных изменений (грязная форма)
В моем приложении Angular 4 у меня есть несколько компонентов с формой, например:
export class MyComponent implements OnInit, FormComponent {
form: FormGroup;
ngOnInit() {
this.form = new FormGroup({...});
}
они используют службу Guard, чтобы предотвратить потерю неподтвержденных изменений, поэтому, если пользователь пытается изменить маршрут до того, как он запросит подтверждение:
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;
}
}
Это с помощью простого confirm(...)
диалог, и он работает просто отлично.
Однако я хотел бы заменить этот простой диалог на более модный модальный диалог, например, используя модальный ngx-bootstrap.
Как я могу достичь того же результата, используя вместо этого модальное?
9 ответов
Я решил это, используя ngx-bootstrap Modals и RxJs Subjects.
Прежде всего я создал модальный компонент:
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();
}
}
вот шаблон:
<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>
Затем я изменил свою охрану, используя тему, теперь это выглядит так:
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;
}
}
Этот метод canDeactivate работает с кодом Франческо Борци, просто расширяя дополнительную информацию, предоставленную mitschmidt относительно кнопки щелчка за пределами / выхода. Я просто добавляю подписку на onHide() в функцию:
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;
}
В дополнение к хорошему решению ShinDarth, кажется, стоит упомянуть, что вам придется покрывать и увольнение модального, потому что метод action() может не сработать (например, если вы разрешите кнопку esc или щелчок за пределами модального окна), В этом случае наблюдаемое никогда не завершается, и ваше приложение может застрять, если вы используете его для маршрутизации.
Я добился этого, подписавшись на bsModalService onHide
Собственность и слияние этого и предмета действия вместе:
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
);
}
В моем случае я сопоставляю onHide
Наблюдаемый как ложный, потому что увольнение считается прерыванием в моем случае (только щелчок "да" даст положительный результат для моего способа подтверждения).
Поскольку я постоянно ходил с Ashwin, я решил опубликовать свое решение с Angular и Material.
Вот мой StackBlitz
Это работает, но я хотел добавить сложность асинхронного ответа со страницы Деактивации, как в моем приложении. Это небольшой процесс, так что потерпите меня, пожалуйста.
Это моя реализация, чтобы получить диалоговое окно подтверждения перед тем, как покинуть определенный маршрут, используя диалоговое окно ngx-bootstrap. У меня есть глобальная переменная под названием "canNavigate" с помощью службы. Эта переменная будет содержать логическое значение, если оно истинно или ложно, чтобы увидеть, возможна ли навигация. Это значение изначально истинно, но если я сделаю изменение в своем компоненте, я сделаю его ложным, поэтому 'canNavigate' будет ложным. Если значение равно false, я открою диалоговое окно, и если пользователь отменит изменения, он пойдет по желаемому маршруту, также приняв queryParams, иначе он не будет маршрутизировать.
@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;
}
}
Вы можете передать значение в
afterClosed
Наблюдаемый диалог:
// modal.component.html
<mat-dialog-actions>
<button mat-button mat-dialog-close>Cancel</button>
<button mat-button [mat-dialog-close]="true">Leave</button>
</mat-dialog-actions>
// unsaved-changes.service.ts
@Injectable({ providedIn: 'root' })
export class UnsavedChangesGuardService
implements CanDeactivate<FormComponent> {
constructor(private _dialog: MatDialog) {}
canDeactivate(component: FormComponent) {
if (component.form.dirty) {
const dialogRef = this._dialog.open(UnsavedChangesDialogComponent);
return dialogRef.afterClosed();
}
return true;
}
}
Для Material Dialog вы можете создать компонент диалогового окна подтверждения с вашим пользовательским HTML, CSS, а затем в режиме защиты вы можете вызвать диалоговое окно, как показано ниже, см.checkConfirmation()
функция:
import { CanDeactivate } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
export interface ifCanDeactivateComponent {
canDeactivate: () => boolean | Observable<boolean> | Promise<boolean>;
}
@Injectable()
export class UnsavedChangesGuard implements CanDeactivate<ifCanDeactivateComponent> {
constructor(public dialog: MatDialog){}
//Confirmation dialog to highlight about any of the unsaved changes
async checkConfirmation(): Promise<boolean> {
let dialogRef = this.dialog.open(ConfirmationDialogComponent, {
disableClose: false,
width: '500px',
});
dialogRef.componentInstance.confirmMessage = 'You have unsaved changes. Are you sure to lose changes?'
let res: boolean = await dialogRef.afterClosed().toPromise();
return res;
}
//Navigation continues if return true, else navigation is cancelled
canDeactivate(component: ifCanDeactivateComponent): boolean | Promise<boolean> {
//Safety check: Check if component implements canDeactivate methods
if(Object.getPrototypeOf(component).hasOwnProperty('canDeactivate')){
// if there are no unsaved changes, allow deactivation; else confirm first
return component.canDeactivate() ? true : this.checkConfirmation();
} else {
throw new Error("This component doesn't implement canDeactivate method");
}
}
}
Вот рабочее решение без темы, вы можете добавить логическое свойство , подтвержденное , чтобы различать, нажал ли пользователь отмену или подтвердил
import { Component, OnInit } from '@angular/core';
import { BsModalRef } from 'ngx-bootstrap/modal';
@Component({
selector: 'app-leave-form-confirmation',
templateUrl: './leave-form-confirmation.component.html',
styleUrls: ['./leave-form-confirmation.component.scss']
})
export class LeaveFormConfirmationComponent implements OnInit {
confirmed = false;
constructor(public bsModalRef: BsModalRef) { }
ngOnInit(): void {
}
confirm = () => {
this.confirmed= true;
this.bsModalRef.hide()
}
}
а вот хтмл
<div class="modal-header">
<h4 class="modal-title pull-left">Confirmation</h4>
</div>
<div class="modal-body">
<h2>Data will be lost, Are you sure to leave the form?</h2>
</div>-*
<div class="modal-footer">
<button type="button" class="btn btn-default" (click)="confirm()">confirm</button>
<button type="button" class="btn btn-default" (click)="bsModalRef.hide()">cancel</button>
</div>
и вот ваш метод canDeactivate
canDeactivate(
component: DataStatus,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
if(!component.isDataSaved()) {
const modalRef = this.modalService.show(LeaveFormConfirmationComponent, {
backdrop: false,
ignoreBackdropClick: true
})
return modalRef.onHidden.pipe(map(_ => modalRef.content.confirmed));
}
return of(true);
}
Я реализовал это решение с помощью Angular Material Dialog:
Модальное окно материала имеет "componentInstance" вместо "content" в модальных окнах ngx-bootstrap:
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;
}