问题
I have a CanDeactive guard based on the Hero app in the official docs.
This guard first checks if my component does not need cleanup, and it immediately returns false if so.
Otherwise it shows a confirm dialog to the user. The dialog is provided by this service (identical to the docs):
import 'rxjs/add/observable/of';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
/**
* Async modal dialog service
* DialogService makes this app easier to test by faking this service.
* TODO: better modal implementation that doesn't use window.confirm
*/
@Injectable()
export class DialogService {
/**
* Ask user to confirm an action. `message` explains the action and choices.
* Returns observable resolving to `true`=confirm or `false`=cancel
*/
confirm(message?: string): Observable<boolean> {
const confirmation = window.confirm(message || 'Is it OK to leave the page?');
return Observable.of(confirmation);
};
}
If the user answers 'No' (false) to the dialog, then CanDeactive also immediately returns false. This is why it has two return types: Observable<boolean> and boolean (again, as per docs).
canDeactivate(): Observable<boolean> | boolean {
console.log('deactivating');
// Allow immediate navigation if no cleanup needed
if (this.template.template_items.filter((obj) =>
{ return !obj.is_completed }).length < 2)
return true;
// Otherwise ask the user with the dialog service
this._dialogService.confirm('You have some empty items.
Is it OK if I delete them?').subscribe(
confirm => {
console.log('confirm is ', confirm);
if (confirm) return this.onDestroyCleanup().subscribe();
else return false;
}
);
}
Where I differ from the docs is that if the user answers yes (true) to the confirm dialog, I need to do some cleanup and then call my api - which you can see is the line return this.onDestroyCleanup().subscribe().
So I don't want to return true immediately, but first call this method and return true or false from that (false only if the api call failed).
Here is that method, which calls the api and maps the result of the api call to true or false:
onDestroyCleanup():Observable<boolean> {
console.log('cleaning up');
this.template.template_items.forEach((item, index) => {
// insert marker flag to delete all incomplete items
if (!item.is_completed) {
this.template.template_items[index]['_destroy'] = true;
};
});
// a final save to remove the incomplete items
return this._templateService.patchTemplate(this.template).take(1).map(
result => {
console.log('cleanup done: ', result);
return true;
},
error => {
console.log('cleanup done: ', error);
this._apiErrorService.notifyErrors(error.json());
return false;
}
);
}
Everything works except the user stays on the page when they answer Yes. The console output is:
deactivating
confirm is true
cleaning up
cleanup done: [returned json object]
Seeing that output I realised that CanDeactive is not returning a result, so I changed the last part of the CanDeactive to this:
...same code up to here...
// Otherwise ask the user with the dialog service
return this._dialogService.confirm('You have some empty items.
Is it OK if I delete them?').map(
confirm => {
console.log('confirm is ', confirm);
if (confirm) this.onDestroyCleanup().subscribe(r => {return r});
else return false;
}
);
}
But I get the same result.
So now I don't know whether I am structuring this correctly or if perhaps my onDestroyCleanup code is the thing that is wrong.
UPDATE
To help understanding, this code obviously works:
// Otherwise ask the user with the dialog service
return this._dialogService.confirm('You have some empty items. Is it
OK if I delete them?').map(
confirm => {
console.log('confirm is ', confirm);
if (confirm) return true;
else return false;
}
);
...because it directly returns either true or false. The problem is how to replace the 'return true' with code that calls another Observable and when that resolves, to return the result of THAT one.
But to illustrate further, this does not work as it says "Type Observable <false> | Observable<boolean> is not assignable to type boolean | Observable<boolean>":
// Otherwise ask the user with the dialog service
return this._dialogService.confirm('You have some empty items. Is it
OK if I delete them?').map(
confirm => {
console.log('confirm is ', confirm);
if (confirm) return Observable.of(true);
else return false;
}
);
回答1:
By doing return this.onDestroyCleanup().subscribe(); in your canDeactivate method, you are returning a subscription, not an observable. Also you need to return a chained observable sequence (with flatMap/mergeMap). This way the canDeactivate method altogether returns an observable that can be subscribed to.
canDeactivate(): Observable<boolean> | boolean {
console.log('deactivating');
// Allow immediate navigation if no cleanup needed
if (this.template.template_items.filter((obj) =>
{ return !obj.is_completed }).length < 2)
return true;
// Otherwise ask the user with the dialog service
return this._dialogService.confirm('You have some empty items. Is it OK if I delete them?')
// chain confirm observable with cleanup observable
.flatMap(confirm => {
console.log('confirm is ', confirm);
if (confirm) {
return this.onDestroyCleanup();
} else {
return Observable.of(false);
}
});
}
As you can see, we are now returning a chained observable sequence that the CanDeactivate Guard can subscribe to.
When you do swap out your confirm you can modify your confirm() method to something like
confirm(message?: string): Observable<boolean> {
return new Observable(observer => {
// bring up custom confirm message somehow
customConfirm(message, (confirmation: boolean) => {
// emit result. This would likely be in a callback function of your custom confirm message implementation
observer.next(confirmation);
observer.complete();
});
});
};
Now your confirm is returning an asynchronous implementation with an observable.
来源:https://stackoverflow.com/questions/47014700/angular4-how-to-make-candeactive-guard-using-dialogservice-confirmation-work-w