Angular 在 canDeactivate Guard 服务中使用模态对话框来处理未提交的更改(表单脏)
Angular use modal dialog in canDeactivate Guard service for unsubmitted changes (Form dirty)
在我的 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 Modal.
我怎样才能使用模态来实现相同的结果?
我用ngx-bootstrap Modals and 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;
}
}
在 app.module.ts 文件中,转到 @NgModule 部分并将 ConfirmLeaveComponent 组件添加到 entryComponents。
@NgModule({
entryComponents: [
ConfirmLeaveComponent,
]
})
这是我使用 ngx-bootstrap 对话框在离开特定路线之前获得确认对话框的实现。在服务的帮助下,我有一个名为 'canNavigate' 的全局变量。如果该变量为 true 或 false,则该变量将保存一个布尔值以查看是否可以导航。该值最初为 true,但如果我对我的组件进行更改,我会将其设为 false,因此 'canNavigate' 将为 false。如果它是 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;
}
}
除了 ShinDarth 的好解决方案之外,似乎值得一提的是您还必须涵盖模态的解雇,因为 action() 方法可能不会被触发(例如,如果您允许 esc 按钮或单击在模态之外)。在这种情况下,observable 永远不会完成,如果您使用它进行路由,您的应用可能会卡住。
我通过订阅 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
observable 映射为 false,因为在我的例子中,解雇被视为中止(仅 'yes' 点击将为我的确认模式产生积极的结果)。
我用 Angular Material 对话框实现了这个解决方案:
Material 的模态有 "componentInstance" 而不是 ngx-bootstrap 模态中的 "content":
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;
}
因为我一直在和 Ashwin 来回交流,所以我决定 post 我的解决方案 Angular 和 Material。
这是我的StackBlitz
这可行,但我想增加来自停用页面的异步响应的复杂性,就像我在我的应用程序中那样。这是一个过程,请耐心等待。
只是扩展了 mitschmidt 提供的关于单击外部/退出按钮的附加信息,此 canDeactivate 方法适用于 Francesco Borzi 的代码。我只是在函数中内联添加对 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;
}
您可以将值传递给对话框的 afterClosed
Observable:
// 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;
}
}
这里是一个没有主题的工作方案,你可以添加一个布尔值属性 confirmed来区分用户点击了取消还是确认
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()
}
}
这里是 html
<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 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 Modal.
我怎样才能使用模态来实现相同的结果?
我用ngx-bootstrap Modals and 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;
}
}
在 app.module.ts 文件中,转到 @NgModule 部分并将 ConfirmLeaveComponent 组件添加到 entryComponents。
@NgModule({
entryComponents: [
ConfirmLeaveComponent,
]
})
这是我使用 ngx-bootstrap 对话框在离开特定路线之前获得确认对话框的实现。在服务的帮助下,我有一个名为 'canNavigate' 的全局变量。如果该变量为 true 或 false,则该变量将保存一个布尔值以查看是否可以导航。该值最初为 true,但如果我对我的组件进行更改,我会将其设为 false,因此 'canNavigate' 将为 false。如果它是 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;
}
}
除了 ShinDarth 的好解决方案之外,似乎值得一提的是您还必须涵盖模态的解雇,因为 action() 方法可能不会被触发(例如,如果您允许 esc 按钮或单击在模态之外)。在这种情况下,observable 永远不会完成,如果您使用它进行路由,您的应用可能会卡住。
我通过订阅 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
observable 映射为 false,因为在我的例子中,解雇被视为中止(仅 'yes' 点击将为我的确认模式产生积极的结果)。
我用 Angular Material 对话框实现了这个解决方案:
Material 的模态有 "componentInstance" 而不是 ngx-bootstrap 模态中的 "content":
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;
}
因为我一直在和 Ashwin 来回交流,所以我决定 post 我的解决方案 Angular 和 Material。
这是我的StackBlitz
这可行,但我想增加来自停用页面的异步响应的复杂性,就像我在我的应用程序中那样。这是一个过程,请耐心等待。
只是扩展了 mitschmidt 提供的关于单击外部/退出按钮的附加信息,此 canDeactivate 方法适用于 Francesco Borzi 的代码。我只是在函数中内联添加对 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;
}
您可以将值传递给对话框的 afterClosed
Observable:
// 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;
}
}
这里是一个没有主题的工作方案,你可以添加一个布尔值属性 confirmed来区分用户点击了取消还是确认
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()
}
}
这里是 html
<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);
}