可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm trying to use the routerCanDeactivate
function for a component in my app. The simple way to use it is as follows:
routerCanDeactivate() {
return confirm('Are you sure you want to leave this screen?');
}
My only issue with this is that it's ugly. It just uses a browser generated confirm prompt. I really want to use a custom modal, like a Bootstrap modal. I have the Bootstrap modal returning a true or false value based on the button they click. The routerCanDeactivate
I'm implementing can accept a true/false value or a promise that resolves to true/false.
Here is the code for the component that has the routerCanDeactivate
method:
export class MyComponent implements CanDeactivate {
private promise: Promise<boolean>;
routerCanDeactivate() {
$('#modal').modal('show');
return this.promise;
}
handleRespone(res: boolean) {
if(res) {
this.promise.resolve(res);
} else {
this.promise.reject(res);
}
}
}
When my TypeScript files compile, I get the following errors in the terminal:
error TS2339: Property 'resolve' does not exist on type 'Promise<boolean>'.
error TS2339: Property 'reject' does not exist on type 'Promise<boolean>'.
When I try to leave the component, the modal starts, but then the component deactivates and doesn't wait for the promise to resolve.
My issue is trying to work out the Promise so that the routerCanDeactivate
method waits for the promise to resolve. Is there a reason why there is an error saying that there is no 'resolve'
property on Promise<boolean>
? If I can work that part out, what must I return in the routerCanDeactivate
method so that it waits for the resolution/rejection of the promise?
For reference, here is the DefinitelyTyped Promise definition. There is clearly a resolve and reject function on there.
Thanks for your help.
UPDATE
Here is the updated file, with the Promise being initialized:
private promise: Promise<boolean> = new Promise(
( resolve: (res: boolean)=> void, reject: (res: boolean)=> void) => {
const res: boolean = false;
resolve(res);
}
);
and the handleResponse
function:
handleResponse(res: boolean) {
console.log('res: ', res);
this.promise.then(res => {
console.log('res: ', res);
});
}
It still doesn't work correctly, but the modal shows up and waits for the response. When you say yes leave, it stays on the component. Also, the first res
that is logged is the correct value returned from the component, but the one inside .then
function is not the same as the one passed in to the handleResponse
function.
More Updates
After doing some more reading, it appears that in the promise
declaration, it sets the resolve
value, and the promise
has that value no matter what. So even though later on I call the .then
method, it doesn't change the value of the promise
and I can't make it true and switch components. Is there a way to make the promise
not have a default value and that it has to wait until the its .then
method is invoked?
Updated functions:
private promise: Promise<boolean> = new Promise((resolve, reject) => resolve(false) );
handleResponse(res: any) {
this.promise.then(val => {
val = res;
});
}
Thanks again for the help.
Last Update
After looking at many suggestions, I decided to create a Deferred
class. It's worked pretty well, but when I do the deferred.reject(anyType)
, I get an error in the console of:
EXCEPTION: Error: Uncaught (in promise): null
This same thing happens when I pass in null
, a string
, or a boolean
. Trying to provide a catch
function in the Deferred
class didn't work.
Deferred Class
export class Deferred<T> {
promise: Promise<T>;
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
回答1:
I'm not familiar with the bootstrap modal api, but I'd expect there to be a way to bind to a close event somehow when creating it.
export class MyComponent implements CanDeactivate {
routerCanDeactivate(): Promise<boolean> {
let $modal = $('#modal').modal();
return new Promise<boolean>((resolve, reject) => {
$modal.on("hidden.bs.modal", result => {
resolve(result.ok);
});
$modal.modal("show");
});
}
}
You're trying to use the Promise
like Deferred
. If you want that kind of API, write yourself a Deferred
class.
class Deferred<T> {
promise: Promise<T>;
resolve: (value?: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
export class MyComponent implements CanDeactivate {
private deferred = new Deferred<boolean>();
routerCanDeactivate(): Promise<boolean> {
$("#modal").modal("show");
return this.deferred.promise;
}
handleRespone(res: boolean): void {
if (res) {
this.deferred.resolve(res);
} else {
this.deferred.reject(res);
}
}
}
回答2:
Is there a reason why there is an error saying that there is no 'resolve' property on Promise?
Yes, and it's that tsc can't find the correct typings for es6-promise
. To avoid this and other typing problems in ng2 projects, as of beta.6 you need to explicitly include
///<reference path="node_modules/angular2/typings/browser.d.ts"/>
somewhere in your application (typically this is done at the top of your main bootstrap file).*
The rest of your question is less clear (and is likely an XY problem where x is the typings problem discussed above). But, if I'm correct in understanding that you've defined a promise like this:
private promise: Promise<boolean> = new Promise(
( resolve: (res: boolean)=> void, reject: (res: boolean)=> void) => {
const res: boolean = false;
resolve(res); // <=== how would this ever resolve to anything but false???
}
);
How are you expecting this to resolve to anything but false?
const res: boolean = false;
resolve(res); //always false
is equivalent to
resolve(false); //always false
*note: this is (presumably) temporary, and won't be necessary in later beta/release versions.
Update in response to your comment:
it doesn't seem obvious how I can wait for the handleResponse function to run and wait for that response
I'm still not clear on what you're trying to do here, but in general, you'd want to have handleResponse
return a promise of its own, and then:
private promise: Promise<boolean> = new Promise((resolve, reject) => {
handlePromise.then(resultOfHandleResult => {
//assuming you need to process this result somehow (otherwise this is redundant)
const res = doSomethingWith(resultOfHandleResult);
resolve(res);
})
});
handleResponse(res: any) {
this.promise.then(val => {
val = res;
});
}
Or, (far) more preferably, use Observables:
var promise = handleResult() //returns an observable
.map(resultOfHandleResult => doSomethingWith(resultOfHandleResult))
回答3:
Here is a technique that worked for me. It is pretty similar to @iliacholy's answer, but uses a modal component instead of a jQuery modal. This makes it a somewhat more "Angular 2" approach. I believe it's still relevant to your question.
First, build an Angular Component for the modal:
import {Component, Output, EventEmitter} from '@angular/core;
@Component({
selector: 'myModal',
template: `<div class="myModal" [hidden]="showModal">
<!-- your modal HTML here... -->
<button type="button" class="btn" (click)="clickedYes()">Yes</button>
<button type="button" class="btn" (click)="clickedNo()">No</button>
</div>
`
})
export class MyModal{
private hideModal: boolean = true;
@Output() buttonResultEmitter = new EventEmitter();
constructor(){}
openModal(){
this.hideModal = false;
}
closeModal(){
this.hideModal = true;
}
clickedYes(){
this.buttonResultEmitter.emit(true);
}
clickedNo(){
this.buttonResultEmitter.emit(false);
}
}
Then on your component with RouterCanDeactivate(), import and reference the MyModal instance:
import {MyModal} from './myModal';
@Component({
....
directives: [MyModal]
})
and in the class code:
private myModal: MyModal;
Create a method returning a promise, which is subscribed to the eventEmitter on myModal:
userClick(): Promise<boolean> {
var prom: new Promise<boolean>((resolve, reject) => {
this.myModal.buttonResultEmitter.subscribe(
(result) => {
if (result == true){
resolve(true);
} else {
reject(false);
}
});
});
return prom;
}
and finally, in the RouterCanDeactivate hook:
routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
this.myModal.openModal();
return this.userClick().catch( function(){return false;} );
}
As @drewmoore mentioned, using Observables is preferred in Angular 2, but A) that wasn't your question, and B) The routerCanDeactivate hook resolves to boolean | Promise, so this approach seemed more natural to me.
回答4:
Since everyone is talking about Observables, I figured that I would take a look and build upon @petryuno1's answer.
Starting with his ModalComponent
:
import {Component, Output, ViewChild} from '@angular/core;
@Component({
selector: 'myModal',
template: `<div class="myModal" [hidden]="showModal">
<!-- your modal HTML here... -->
<button type="button" class="btn" (click)="clickedYes($event)">Yes</button>
<button type="button" class="btn" (click)="clickedNo($event)">No</button>
</div>
`
})
export class MyModal{
private hideModal: boolean = true;
private clickStream = new Subject<boolean>();
@Output() observable = this.clickStream.asObservable();
constructor(){}
openModal(){
this.hideModal = false;
}
closeModal(){
this.hideModal = true;
}
clickedYes(){
this.clickStream.next(true);
}
clickedNo(){
this.clickStream.next(false);
}
}
Next, we go to the AppComponent
:
import { Component, ViewChild} from '@angular/core';
import {MyModal} from './myModal';
import {Subscription} from "rxjs";
@Component({
....
directives: [MyModal]
})
export class AppComponent {
@ViewChild(ConfirmModal) confirmModal: ConfirmModal;
constructor(){...};
public showModal(){
this.myModal.openModal();
this.subscription = this.myModal.observable.subscribe(x => {
console.log('observable called ' + x)
// unsubscribe is necessary such that the observable doesn't keep racking up listeners
this.subscription.unsubscribe();
});
}
}
The elegance of observables is that we now get to write a lot less code to do the same thing.
回答5:
Can also be done out of the box in the Angular2+ world using Subject
s:
export class MyComponent {
private subject: Subject<boolean>;
routerCanDeactivate(): PromiseLike<boolean> {
$('#modal').modal('show');
return this.subject.toPromise();
}
handleRespone(res: boolean) {
this.subject.next(res);
}
}