27

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;
        });
    }
}
7
  • 3
    I might be wrong since I don't know typescript, but shouldn't you first initialize this.promise? More accurately, shouldn't you return a promise, and keep reference of its resolve & reject functions, then call these? Commented Apr 13, 2016 at 21:45
  • @Amit you are correct, I updated the original post. Commented Apr 13, 2016 at 22:01
  • what's invoking handle response? Commented Apr 13, 2016 at 22:27
  • @iliacholy That's a function that is called when the modal is closed. It's an output of an Angular 2 Modal Component I made. I've also confirmed that it is being called correctly and the response is coming in. Commented Apr 13, 2016 at 22:28
  • Not an answer to your question, but your promise definition is extremely verbose. It can be private promise = new Promise<boolean>((resolve, reject) => resolve(false)); Commented Apr 13, 2016 at 22:36

5 Answers 5

29

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);
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the suggestion! It's really close to working. The deferred.reject errors for some reason, and if you resolve with false it kind of freaks out too, so I just have to figure out why it does that.
I finally circled back to this issue on my project, and this works the best. Making a Deferred class didn't work before when I tried it, but when I used your top suggestion of listening for the event when the modal is closed, that part worked for me. Thanks!
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 Comments

So as I started doing more digging, I realized that I was setting it to always be false, but it doesn't seem obvious how I can wait for the handleResponse function to run and wait for that response before the promise is resolved. Also, I think the error came from not correctly initializing the promise instance, which I believe I've fixed.
@pjlamb12 see edit... is that what you're looking for?
I do need a way to make the routerCanDeactivate method wait to resolve to true or false based on the handleResult method, I'm just not sure the best way to go about it. The answer above with the Deferred class almost works, but there's an error when the promise is resolved to false or rejected, so it turned out not to work.
2

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.

2 Comments

Thanks for the comment I will definitely try this and get back to you with how it works for me.
This is a fantastic response. I had to modify the userClick() function to also remember the subscription, so that I could unsubscribe. this.subscription = this.myModal.buttonResultEmitter.subscribe(...). Also in routerCanDeactivate, I needed a section of this.userClick().then(result=>{console.log(result); this.subscription.unsubscribe();})
2

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.

Comments

0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.