web-dev-qa-db-fra.com

Angular 2 et TypeScript Promises

J'essaie d'utiliser la fonction routerCanDeactivate pour un composant de mon application. La façon simple de l'utiliser est la suivante: 

routerCanDeactivate() {
    return confirm('Are you sure you want to leave this screen?');
}

Mon seul problème avec ça, c'est que c'est moche. Il utilise simplement une invite de confirmation générée par le navigateur. Je veux vraiment utiliser un modal personnalisé, comme un modal Bootstrap. J'ai la modale Bootstrap renvoyant une valeur true ou false en fonction du bouton sur lequel ils cliquent. La routerCanDeactivate que j'implémente peut accepter une valeur true/false ou une promesse résolue en true/false. 

Voici le code du composant qui a la méthode routerCanDeactivate:

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);
        }
    }
}

Lorsque mes fichiers TypeScript sont compilés, les erreurs suivantes apparaissent dans le terminal: 

error TS2339: Property 'resolve' does not exist on type 'Promise<boolean>'.
error TS2339: Property 'reject' does not exist on type 'Promise<boolean>'.

Lorsque j'essaie de quitter le composant, le modal démarre, mais le composant se désactive et n'attend pas que la promesse soit résolue. 

Mon problème est d’essayer d’élaborer la promesse de manière à ce que la méthode routerCanDeactivate attende que la promesse soit résolue. Y a-t-il une raison pour laquelle il y a une erreur disant qu'il n'y a pas de propriété 'resolve' sur Promise<boolean>? Si je peux travailler sur cette partie, que dois-je retourner dans la méthode routerCanDeactivate pour qu’elle attend la résolution/le rejet de la promesse?

Pour référence, voici la définition de DefinitelyTyped Promise . Il y a clairement une fonction de résolution et de rejet sur celui-ci.

Merci de votre aide.

METTRE &AGRAVE; JOUR

Voici le fichier mis à jour, avec la promesse en cours d'initialisation:

private promise: Promise<boolean> = new Promise(
    ( resolve: (res: boolean)=> void, reject: (res: boolean)=> void) => {
        const res: boolean = false;
        resolve(res);
    }
);

et la fonction handleResponse

handleResponse(res: boolean) {
    console.log('res: ', res);
    this.promise.then(res => {
        console.log('res: ', res);
    });
}

Cela ne fonctionne toujours pas correctement, mais le modal apparaît et attend la réponse. Lorsque vous dites oui, quittez, il reste sur le composant. De même, la première res enregistrée est la valeur correcte renvoyée par le composant, mais celle qui se trouve dans la fonction .then n'est pas identique à celle transmise à la fonction handleResponse.

Autres mises à jour

Après avoir lu un peu plus, il apparaît que dans la déclaration promise, la valeur resolve est définie et que promise a cette valeur, quoi qu’il en soit. Donc même si j'appelle plus tard la méthode .then, cela ne change pas la valeur de promise et je ne peux pas le rendre vrai ni changer de composant. Existe-t-il un moyen de faire en sorte que promise n'ait pas de valeur par défaut et qu'il doive attendre que la méthode .then soit appelée?

Fonctions mises à jour:

private promise: Promise<boolean> = new Promise((resolve, reject) => resolve(false) );

handleResponse(res: any) {
    this.promise.then(val => {
        val = res;
    });
}

Merci encore pour votre aide.

Dernière mise à jour

Après avoir examiné de nombreuses suggestions, j'ai décidé de créer une classe Deferred. Cela a plutôt bien fonctionné, mais lorsque je fais la deferred.reject(anyType), une erreur se produit dans la console de:

EXCEPTION: Error: Uncaught (in promise): null

La même chose se produit lorsque je passe dans null, un string ou un boolean. Essayer de fournir une fonction catch dans la classe Deferred n'a pas fonctionné.

Classe différée

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;
        });
    }
}
25
pjlamb12

Je ne connais pas bien l'API modale bootstrap, mais je m'attendrais à ce qu'il y ait un moyen de se lier à un événement proche d'une manière ou d'une autre lors de sa création.

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");
    });
  }

}

Vous essayez d'utiliser la variable Promise telle que Deferred. Si vous voulez ce type d’API, écrivez vous-même une classe Deferred.

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);
        }
    }
}
28
Ilia Choly

Puisque tout le monde parle d'Observables, j'ai décidé de jeter un coup d'œil sur la réponse de @ petryuno1.

Commençant par sa 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);
    }
}

Ensuite, nous allons à la 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();
        });
    }
}

L’élégance des observables réside dans le fait qu’il faut désormais écrire beaucoup moins de code pour faire la même chose.

2
Jon

Voici une technique qui a fonctionné pour moi. C'est assez similaire à la réponse de @ iliacholy, mais utilise un composant modal au lieu d'un modal jQuery. Cela en fait une approche un peu plus "angulaire 2". Je crois que cela reste pertinent pour votre question.

Commencez par construire un composant angulaire pour le 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);
    }
}

Ensuite, sur votre composant avec RouterCanDeactivate (), importez et référencez l'instance MyModal:

import {MyModal} from './myModal';
@Component({
    ....
    directives: [MyModal]
})

et dans le code de la classe:

private myModal: MyModal;

Créez une méthode renvoyant une promesse, qui est abonnée à eventEmitter sur 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;
}

et enfin, dans le hook RouterCanDeactivate:

routerCanDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
    this.myModal.openModal();
    return this.userClick().catch( function(){return false;} );
}

Comme @drewmoore l'a mentionné, l'utilisation d'Observables est préférable dans Angular 2, mais A), ce n'était pas votre question, et B) Le hook routerCanDeactivate résout en booléen | Promise, cette approche me paraissait plus naturelle.

2
petryuno1

Y a-t-il une raison pour laquelle il y a une erreur disant qu'il n'y a pas de propriété 'résoudre' sur Promise? 

Oui, et c’est que tsc ne peut pas trouver les typages corrects pour es6-promise. Pour éviter cela et d’autres problèmes de frappe dans les projets ng2, à partir de la version bêta.6 , vous devez inclure explicitement 

///<reference path="node_modules/angular2/typings/browser.d.ts"/>

quelque part dans votre application (généralement, cela se fait en haut de votre fichier de démarrage principal). *

Le reste de votre question est moins clair (et est probablement un problème XY où x est le problème de typage discuté ci-dessus). Mais si je comprends bien, vous avez défini une promesse comme celle-ci: 

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??? 
    }
);

Comment vous attendez-vous à ce que cela résout autre chose que faux? 

const res: boolean = false;
resolve(res); //always false

est équivalent à 

resolve(false);  //always false

* Remarque: ceci est (vraisemblablement) temporaire et ne sera pas nécessaire dans les versions bêta/release ultérieures. 

Mise à jour en réponse à votre commentaire: 

il ne semble pas évident de savoir comment je peux attendre que la fonction handleResponse s'exécute et d'attendre cette réponse 

Je ne comprends toujours pas bien ce que vous essayez de faire ici, mais en général, vous voudriez que handleResponse rende sa propre promesse, puis: 

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;
    });
}

Ou, (far) plus préférablement, utilisez Observables: 

var promise = handleResult() //returns an observable
                .map(resultOfHandleResult => doSomethingWith(resultOfHandleResult))
2
drewmoore

Peut également être effectué hors de la boîte dans le monde Angular2 + en utilisant 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);
  }
}
0
Saeb Amini