Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ionic 2: Modal window doesn't dismiss when overriding back button on Android device. #6982

Closed
daveshirman opened this issue Jun 20, 2016 · 44 comments
Labels
needs: reply the issue needs a response from the user

Comments

@daveshirman
Copy link

Short description of the problem:

If you override the back button e.g.:

  registerBackButtonListener() {
    this.platform.registerBackButtonAction(() => {
      var nav = this.getNav();
      if (nav.canGoBack()) {
        nav.pop();
      }
      else {
        //... do something else...
      }
    });
  }

If a modal window is open, it is not closed and instead the page in the background will navigate back.

What behavior are you expecting?

The line: nav.canGoBack() should pop a modal view, not the page underneath.

Which Ionic Version? 1.x or 2.x
2

Run ionic info from terminal/cmd prompt: (paste output below)
Your system information:

Cordova CLI: 6.1.1
Ionic Framework Version: 2.0.0-beta.9
Ionic CLI Version: 2.0.0-beta.25
Ionic App Lib Version: 2.0.0-beta.15
ios-deploy version: 1.8.6
ios-sim version: 5.0.8
OS: Mac OS X El Capitan
Node Version: v4.3.0
Xcode version: Xcode 7.2.1 Build version 7C1002

@daveshirman
Copy link
Author

Hi guys, can someone tell me if I'm the problem here or if it's the framework? I just want to override the back button to catch app exiting, so if I'm doing something wrong, please tell me.

@jgw96
Copy link
Contributor

jgw96 commented Jun 23, 2016

Hello @daveshirman ! Thanks for opening an issue with us. A modal is actually an instance of viewController and not navController, so something like this should work for you:

this.platform.registerBackButtonAction(() => {
      this.viewController.dismiss()
      else {
        //... do something else...
      }
    });

Thanks for using Ionic and sorry for the delay on this!

@jgw96 jgw96 closed this as completed Jun 23, 2016
@daveshirman
Copy link
Author

Hi, thanks for your reply. But how does this actually solve the problem? I mean, how do I know programmatically whether I need to dismiss()a viewController OR check nav.canGoBack()?

This seems like something the platform should supply (already does?).

@jgw96
Copy link
Contributor

jgw96 commented Jun 24, 2016

Sorry for the misunderstanding! If you would like to catch a user closing your app you can simply listen for the pause cordova event. Hope that explains things better!

@jgw96
Copy link
Contributor

jgw96 commented Jun 24, 2016

Hello, i am currently discussing this with my team. Also sorry for misunderstanding your issue, i understood the issue to be about catching an app exiting, my mistake.

@daveshirman
Copy link
Author

Hi, what was the result of the discussion with your team? Is there a consensus on how to override the back button without losing basic page back, modal dismissing and alert dismissing functionality?

@daveshirman
Copy link
Author

Hi, can someone please reply to me from the Ionic team? Thanks.

@jgw96
Copy link
Contributor

jgw96 commented Jul 5, 2016

Hello @daveshirman ! Sorry for the delay on this issue, with the release of beta.10 last week i have been very busy with things related to the release. I plan on taking another look at this issue today. Thanks!

@jgw96 jgw96 added the needs: reply the issue needs a response from the user label Jul 5, 2016
@jgw96
Copy link
Contributor

jgw96 commented Jul 6, 2016

Hey @daveshirman at this time there is not a good, clean way to programatically detect if their is an overlay component (like modals or alerts) open (although it is something we are considering). For a workaround i would try something like

this.platform.registerBackButtonAction(() => {
  try {
    this.viewController.dismiss()
  }
  catch(e) {
    ... no overlay component open
  }
})

Sorry for any hassle this causes, i will be closing this issue for now as it is not something that we plan on implementing soon, but will reopen this issue if something changes. Thanks for using Ionic, and again, sorry for the delay on this.

@jgw96 jgw96 closed this as completed Jul 6, 2016
@Chuckv01
Copy link

@daveshirman Did you ever find a good solution to this issue? We have been running into this issue too. Very annoying.

@daveshirman
Copy link
Author

Hi, unfortunately not. My "solution"was to abandon overriding the back
button as it doesn't work reliably.

Don't know when/whether the guys at Ionic are intending to fix it either.

On 10 Sep 2016 2:02 a.m., "Chuckv01" [email protected] wrote:

@daveshirman https://github.com/daveshirman Did you ever find a good
solution to this issue? We have been running into this issue too. Very
annoying.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#6982 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/ANnlXmIsXWVvJwZnMuSUFD0Em2pSvBt2ks5qogGZgaJpZM4I6Pdr
.

@imcosta
Copy link

imcosta commented Sep 29, 2016

I had the same problem. My "solution" was: if their is an overlay component, the ion-app element has the class "disable-scroll", so i can check if the class if present or not to prevent go back.

Something like this:

this.platform.registerBackButtonAction(() => {

          //https://github.com/driftyco/ionic/issues/6982
          let element = document.getElementsByTagName('ion-app')[0];
          let existOverlay = element.classList.contains('disable-scroll');

          if(! existOverlay) {
            let nav = app.getRootNav();

            if(nav.canGoBack()) 
            {
                nav.pop();
            }
            else
            {
              this.confirmExit();
              return false;
            }
          }
        }, 100)

I know that is not a beautiful solution but it works. In the confirm method I show the confirm modal. Also I used a flag to prevent that users press back repeatedly and multiples modal appear.

@laserus
Copy link

laserus commented Oct 18, 2016

OK, I needed it and have a workaround:

Just dismiss portals yourself:

import { Component, ViewChild } from '@angular/core';
import { Platform, Events, App, MenuController, AlertController, Nav, IonicApp } from 'ionic-angular';
import { StatusBar } from 'ionic-native';

import { TabsPage } from '../pages/tabs/tabs';


@Component({
  template: `<ion-nav id="nav" #content [root]="rootPage"></ion-nav>`
})
export class MyApp {
  rootPage = TabsPage;
  @ViewChild(Nav) nav: Nav;

  constructor(private platform: Platform, private alertCtrl: AlertController,
    public events: Events, private app: App, private ionicApp: IonicApp,
    private menuCtrl: MenuController) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      StatusBar.styleDefault();


      let ready = true;
      document.onkeypress = (event) => {
        if (!event) return;

        if (event.keyCode >= 48 && event.keyCode <= 57) {

          console.log(this.app);
          console.log(this.ionicApp);
          console.log(this.nav);

          let activePortal = this.ionicApp._loadingPortal.getActive() ||
            this.ionicApp._modalPortal.getActive() ||
            this.ionicApp._toastPortal.getActive() ||
            this.ionicApp._overlayPortal.getActive();

          if (activePortal) {
            ready = false;
            activePortal.dismiss();
            activePortal.onDidDismiss(() => { ready = true; });
          }
          else { .... your default code here }
        }
      };
    });
  }
}

For testing I used html windows and keypress event, but you can define this function for registering back button. There is ready param, which do not try to dismiss portal if we in process of dismiss...

@Chuckv01
Copy link

@laserus that looks promising. I will have to try it out.

@jgw96 This is still an issue for us as of RC1. I don't think the issue should be closed. We've been forced to stop overriding the android back button as modals and dialogues still do not behave properly when it is overridden and the hardware back button is pressed.

@lazy-ape
Copy link

Hi, I find a way to solve it in ionic2. I learn it from Ionic Source. I check if having overlay is showing like this:

const portal = app._appRoot._getPortal();
if(portal.length() > 0){
    //have overlay
    portal.pop();
}

So, I override the back button to catch app exiting like this:

platform.registerBackButtonAction(() => {
        let activeVC = this.nav.getActive();
        let page = activeVC.instance;
        if (page instanceof TabsPage) {
          try {
            const portal = app._appRoot._getPortal();
            if (portal.length() == 0) {
                // no overlay
                // do something  you want
                showExitingConfirm();
            }
          } catch (e) {
              console.log("back action listener error :" + e);
          }
        }
        //  the default handler
        app.navPop();
      });

My english is pool. Hope it can help you.

@laserus
Copy link

laserus commented Oct 19, 2016

@lazy-ape I think currently there is a bug #8692 that modal is not closing. So you should treat it outside of app.navPop(), but otherwise it is good.

Simplest way is to do this.ionicApp._modalPortal.getActive() and dismiss it yourself.

I have a similar approach (without modals yet) but instead of if (page instanceof TabsPage) { I put on pages confirmation variable "needConfirmationOnExit" (but more appropriate approach is to use some base class and extend).

if( page && page.needConfirmationOnExit) { /* special treatment */ }
else {....}

@jrierab
Copy link

jrierab commented Oct 28, 2016

@laserus thanks for posting your workaround. The activePortal part solved my issues with modals.

@xggaxlc
Copy link

xggaxlc commented Nov 8, 2016

@laserus thanks. works fine for me!

@sunnyahoo
Copy link

Greatly helped! Thanks a lot.

@laserus
Copy link

laserus commented Nov 16, 2016

I see that others are using my piece of code and want to warn about one caveat: the onDidDismiss callback is only one per modal...

so I had one inside one component that returned value from modal an pushed it to observer and then complete the observer (memory leak). And this one in backbuttonhandler which doing nothing atm. And this one broke the whole app logic flow.

To solve it, I had to made some changes to it, but I guess onDidDismiss could completely be removed here.... need to be tested (if it affects your app)

@PurpleEdge2214
Copy link

Newby question...

Where do I implement this code? Do I need it on every window, or can I add it once globally somewhere?

@laserus
Copy link

laserus commented Dec 14, 2016

@PurpleEdge2214 you can put it once in bootstrap app.component.ts (or whatever is your first component). Inside constructor.

@PurpleEdge2214
Copy link

Thanks, will do!

@sarfraaz-talat
Copy link

@laserus from where to import those services ConfigService,NfcService etc.. I found error when using those in constructor, Thanks.

@laserus
Copy link

laserus commented Dec 27, 2016

@sarfraaz53 You should remove them from your code. That is piece of my code with my services. You do not need them... I just shown constructor function in order to show where you place the code, ignore everything unknown.

@hayuki
Copy link

hayuki commented Feb 10, 2017

@laserus - in your comment about the caveat in your fix you said "To solve it, I had to made some changes to it, but I guess onDidDismiss could completely be removed here.... need to be tested". What exactly did you have to change to solve the onDidDismiss issue? I am having the same problem - once I dismiss the modal once with the back button, no modals show after that. Could you perhaps give some ideas?

@laserus
Copy link

laserus commented Feb 13, 2017

@hayuki just remove: activePortal.onDidDismiss(() => { ready = true; });

ready is not used anyway...

@julien-tmp
Copy link

If this can help, that's what I have for the current Ionic 2 version, based on @laserus 's code.
My use case here is that I want the user to be directed back to the home page if they have clicked a menu item.

 constructor(public platform: Platform, private ionicApp: IonicApp, private menuCtrl: MenuController) {
    this.initializeApp();

    // used for an example of ngFor and navigation
    this.pages = [
      { title: 'Page One', component: Page1, name: 'Page1' },
      { title: 'Page Two', component: Page2, name: 'Page2'}
    ];
  }

  initializeApp() {
    this.platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.

      StatusBar.styleDefault();
      // Splashscreen.hide();
      this.hideSplashScreen();

      // let view = this.nav.getActive();
      // let currentRootPage = view.component.name;

      this.nav.getByIndex(0).component.id=1;
      this.platform.registerBackButtonAction(() => {

        let activePortal = this.ionicApp._loadingPortal.getActive() ||
          this.ionicApp._modalPortal.getActive() ||
          this.ionicApp._toastPortal.getActive() ||
          this.ionicApp._overlayPortal.getActive();

        let view = this.nav.getActive();
        let currentRootPage = view.component;


        if (activePortal) {
          activePortal.dismiss();
        }
        else if (this.menuCtrl.isOpen()) {
          this.menuCtrl.close();
        }
        else if (this.nav.canGoBack() || view && view.isOverlay) {
          this.nav.pop();
        }
        else if(currentRootPage != this.pages[0].component) { // Could any other page that you consider as your main one
          this.openPage(this.pages[0]);
        }
        else {
          this.platform.exitApp();
        }

        return;

      }, 1);

    });
  }

@leonardocaldas
Copy link

leonardocaldas commented Apr 1, 2017

Hey guys, here is my final solution, thanks @laserus for your help!

platform.registerBackButtonAction(() => {
    let activePortal = ionicApp._loadingPortal.getActive() ||
        ionicApp._modalPortal.getActive() ||
        ionicApp._toastPortal.getActive() ||
        ionicApp._overlayPortal.getActive();

    if (activePortal) {
        return activePortal.dismiss();
    }

    let navChild = this.nav.getActiveChildNav();

    while (true) {
        if (!navChild) {
            break;
        }

        if (navChild.canGoBack()) {
            return navChild.pop();
        }

        navChild = navChild.getActiveChildNav();
    }

    if (this.nav.canGoBack()) {
        return this.nav.pop();
    }
    
    if (menu.isOpen()) {
        return menu.close();
    }

    this.confirmExitApp();
}, 0);

@gentlemanjohn
Copy link

gentlemanjohn commented Apr 16, 2017

This solution was working great up until a recent update (2.2.0+):

let activePortal = this.ionicApp._loadingPortal.getActive() ||
    this.ionicApp._modalPortal.getActive() ||
    this.ionicApp._toastPortal.getActive() ||
    this.ionicApp._overlayPortal.getActive();

if (activePortal) {
    activePortal.dismiss()
}

Now activePortal is always undefined even if I have modals open. I don't believe anything else has changed in my app. Anyone else encountering this?

@laserus
Copy link

laserus commented Apr 16, 2017

@gentlemanjohn Judging from the code it should work.

If you open ionic/src/components/app/app-root.ts, you can see that all portals are generated in template:

    '<div #viewport app-viewport></div>' +
    '<div #modalPortal overlay-portal></div>' +
    '<div #overlayPortal overlay-portal></div>' +
    '<div #loadingPortal class="loading-portal" overlay-portal></div>' +
    '<div #toastPortal class="toast-portal" [overlay-portal]="10000"></div>' +
    '<div class="click-block"></div>'

And next they are referenced as:

@ViewChild('modalPortal', { read: OverlayPortal }) _modalPortal: OverlayPortal;

OverlayPortal extends NavControllerBase which has getActive()

The only strange thing in above code is that [overlay-portal]="10000" for toast, I guess it is a bug. Probably it does not work for toast at all. By mistake instead of defining attribute it was defined as Input of some non-existing angular2 component.
Actually, not. There is also input @input('overlay-portal') for overlay-portal

@danillo10
Copy link

thanks to all!!

@xiaosan666
Copy link

xiaosan666 commented Apr 18, 2017 via email

@mayurmarwa
Copy link

mayurmarwa commented Apr 20, 2017

Hi guys, I got it working perfectly for multiple tabs, modals, menu , keyboard and other overlays with help from all the above comments. Here's the working code for Ionic 3.0.0:

`
this.platform.registerBackButtonAction(() => {

        if (this.keyboard.isOpen()) { // Handles the keyboard if open
            this.keyboard.close();
            return;
        }

        let activePortal = this.ionicApp._loadingPortal.getActive() ||
           this.ionicApp._modalPortal.getActive() ||
           this.ionicApp._toastPortal.getActive() ||
            this.ionicApp._overlayPortal.getActive();
    
       //activePortal is the active overlay like a modal,toast,etc
        if (activePortal) {
            activePortal.dismiss();
            return
        }
        else if (this.menuCtrl.isOpen()) { // Close menu if open
            this.menuCtrl.close();
            return
        }

        let view = this.nav.getActive(); // As none of the above have occurred, its either a page pushed from menu or tab
        let activeVC = this.nav.getActive(); //get the active view
       
        let page = activeVC.instance; //page is the current view's instance i.e the current component I suppose
                 

        if (!(page instanceof TabsPage)) { // Check if the current page is pushed from a menu click
            
            if (this.nav.canGoBack() || view && view.isOverlay) {
                this.nav.pop(); //pop if page can go back or if its an overlay over a menu page
            }             
            else {
                this.showAlert();
            }

            return;
        }
        
        let tabs = this.app.getActiveNav(); // So it must be a view from a tab. The current tab's nav can be accessed by this.app.getActiveNav();
      

        if (!tabs.canGoBack()) {
          
            return this.showExitAlert();
        }

      
        return tabs.pop();

        
    }, 0);
   ` 

This should work out all possible scenarios, let me know if somethig is amiss.

@jd0048
Copy link

jd0048 commented May 24, 2017

@laserus solution works like charm thanx buddy

@vptcnt
Copy link

vptcnt commented Jun 24, 2017

@mayurmarwa your code seems nice but I don't understand the two lines:

let view = this.nav.getActive(); // As none of the above have occurred, its either a page pushed from menu or tab
let activeVC = this.nav.getActive(); //get the active view
...

same variable attribution..

@piernik
Copy link

piernik commented Feb 22, 2018

Maybe a good idea is to introduce new event beforeAppClose? In which You could return false; to prevent from closing?

@mdarshath
Copy link

mdarshath commented Mar 29, 2018

Checked on Real Devices Working Perfetly

//Check Hardware Back Button Double Tap to Exit And Close Modal On Hardware Back

        let lastTimeBackPress = 0;
       let timePeriodToExit = 2000;
       this.platform.registerBackButtonAction(() => {
          let activePortal = this.ionicApp._loadingPortal.getActive() || // Close If Any Loader Active
          this.ionicApp._modalPortal.getActive() ||  // Close If Any Modal Active
          this.ionicApp._overlayPortal.getActive(); // Close If Any Overlay Active

          if (activePortal) {
              activePortal.dismiss();
          }
          else if(this.nav.canGoBack()){
            this.nav.pop();
          }else{
              //Double check to exit app
              if (new Date().getTime() - lastTimeBackPress < timePeriodToExit) {
                  this.platform.exitApp(); //Exit from app
              } else {
                this.toast.create("Press back button again to exit");
                lastTimeBackPress = new Date().getTime();
              }
          }            
      });

@mcornillon87
Copy link

Found a solution by creating a provider with an observable.
When back bouton is pressed from modal, it reset backbouton on classic page.

// BACK PROVIDER
export class BackProvider {
backPressed: BehaviorSubject;

constructor(public http: HttpClient) {
console.log('Hello BackProvider Provider');
this.backPressed = new BehaviorSubject(false);
}

setBack(val){
if(val == true){
this.backPressed.next(true);
console.log('BACKPROVIDER SETBACK');
}
}

getBack(){
console.log('BACKPROVIDER GETBACK');
return this.backPressed.asObservable();
}

// ANY CLASSIC PAGE
constructor(public navCtrl: NavController, public navParams: NavParams, public modalCtrl: ModalController, public viewCtrl: ViewController, private backProvider: BackProvider, private platform: Platform) {
this.autoRegisterBack();
}

back(){
this.navCtrl.pop();
}

ionViewWillEnter(){
console.log('ionWillEnter');
this.platform.registerBackButtonAction(() => {
console.log('this.back called (team member page)');
this.back();
});
}

autoRegisterBack(){
this.backProvider.getBack().subscribe(val => {
console.log('autoregisterBack prend true');
if(val == true){
this.platform.registerBackButtonAction(() => {
this.back();
});
}
});
}

// MODAL PAGE
constructor(public navCtrl: NavController, public navParams: NavParams, public viewCtrl: ViewController, private backProvider: BackProvider, private platform: Platform) {
}

ionViewWillEnter(){
this.platform.registerBackButtonAction(() => {
this.dismiss();
});
}

dismiss(){
this.viewCtrl.dismiss();
this.backProvider.setBack(true);
}

@Riyaz0001
Copy link

Thanks Man, Helped Again @laserus

@ionitron-bot
Copy link

ionitron-bot bot commented Sep 1, 2018

Thanks for the issue! This issue is being locked to prevent comments that are not relevant to the original issue. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.

@ionitron-bot ionitron-bot bot locked and limited conversation to collaborators Sep 1, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
needs: reply the issue needs a response from the user
Projects
None yet
Development

No branches or pull requests