Angular 2 and TypeScript Promises

2020-05-21 09:11发布

问题:

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 Subjects:

export class MyComponent {
  private subject: Subject<boolean>;

  routerCanDeactivate(): PromiseLike<boolean> {
    $('#modal').modal('show');
    return this.subject.toPromise();
  }

  handleRespone(res: boolean) {
    this.subject.next(res);
  }
}