Ionic-framework: feat: Back button behavior for browser

Created on 28 Apr 2016  ·  26Comments  ·  Source: ionic-team/ionic-framework

Type: feat

Ionic Version: 2.x

Platform: desktop browser

In #5071, the back button behavior for platforms are fixed. However, it does not fix the behavior for platform browser or mobile web.

Expected behavior: browser back button works the same as hardware back buttons in Ionic 2 apps.

There are quite a number of Ionic developers using Ionic for mobile web application. This Ionic 1 feature should exist in Ionic 2 despite the difference in navigation mechanism.

Most helpful comment

As Ionic had a big announcment that it will be supporting Progressive Web Apps, I really wonder why this issue is not on the Roadmap. Or in any other discussion.
In my opinion this is nearly a showstopper for PWA with Ionic. Because navigation elements that the user is used to (browser back-button & hardware back-button) will actually "close" the application.
Please leave a statement for the plans on this.

All 26 comments

As Ionic had a big announcment that it will be supporting Progressive Web Apps, I really wonder why this issue is not on the Roadmap. Or in any other discussion.
In my opinion this is nearly a showstopper for PWA with Ionic. Because navigation elements that the user is used to (browser back-button & hardware back-button) will actually "close" the application.
Please leave a statement for the plans on this.

I agree with @samuba - please let us know your plans! Should we work on a workaround or wait for official solution?

I understand that Browser history won't be able to support all features of NavController. But, that's okay as the main thing is to prevent navigation away from the the App on back button - which is very confusing for the user.

And, having deep linking would be nice too.

Hello! This is currently on our roadmap and actively being worked on. Thanks!

Hi,
is there already a solution for this? Currently with Ionic v2 RC0 when I open the app in the mobile browser (Chrome on Android), navigate to a sub-page and tap the "hardware" back button, I exit the app, which is not reasonable for users.

Or is there at least a workaround?

+1 - Is there a solution for this? Thanks

+1 - any solution yet ? Can team suggest any work around ?

Anyone find a solution for this?

@patrickmcd seriously why you thumb down people ! no comment too !

Is there anybody who has solved this problem ?

I stumbled upon this issue this morning. Using HTML5 browser history API and some Ionic magic I managed to get it working with just a few lines of code. I haven't thoroughly tested this yet, but it does appear like it's working on Chrome and Safari. Enjoy!

https://gist.github.com/t00ts/3542ac4573ffbc73745641fa269326b8

Thanks by this solution!!!

2016-11-17 10:15 GMT-02:00 Abel Elbaile [email protected]:

I stumbled upon this issue this morning. Using HTML5 browser history API
and some Ionic magic I managed to get it working with just a few lines of
code. I haven't thoroughly tested this yet, but it does appear like it's
working on Chrome and Safari. Enjoy!

https://gist.github.com/t00ts/3542ac4573ffbc73745641fa269326b8


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/driftyco/ionic/issues/6363#issuecomment-261232421,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ALYYXTBkh5MuvbZ1VIJtti6pm7R0JTc4ks5q_EVwgaJpZM4ISGTk
.

Acesse as páginas da nossa empresa e conheça os aplicativos que vão te
ajudar a economizar, a estar bem informado e muito mais! Aguarde
novidades...

[image: Vida App Fanpage] https://goo.gl/o3SdRs
[image: Vidaapp Linkedin Company Page] https://goo.gl/LNmTy7

Acesse também o nosso site: Vida App http://vidaapp.com.br

@t00ts Thanks for the solution. However I can't seem to make it work. this._app.getActiveNav().canGoBack() is always false even when I have a page loaded. How to make this work when I have a nav with tabs on the loaded page?

@BAWES Could you try replacing that piece of code with this._app.getRootNav().canGoBack().

@t00ts That doesn't work either for my applications inner pages.
Your code functions perfectly on the initial pages, however once I load a <nav> which has menus and load a Tabs page within it, it doesn't seem to be able to reach the navCtrl from there.

I was able to make it function by passing the navCtrl from the loaded page to the app component through the Events pub/sub Ionic2 component.

In my app.component.ts

private _innerNavCtrl;

private _setupBrowserBackButtonBehavior () {

    // If on web version (browser)
    if (window.location.protocol !== "file:") {

      // Listen to browser pages
      this._events.subscribe("navController:current", (navCtrlData) => {
        this._innerNavCtrl = navCtrlData[0];
      });

      // Register browser back button action(s)
      window.onpopstate = (evt) => {

        // Close menu if open
        if (this._menu.isOpen()) {
          this._menu.close ();
          return;
        }

        // Close any active modals or overlays
        let activePortal = this._ionicApp._loadingPortal.getActive() ||
          this._ionicApp._modalPortal.getActive() ||
          this._ionicApp._toastPortal.getActive() ||
          this._ionicApp._overlayPortal.getActive();

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

        // Navigate back on main active nav if there's a page loaded
        if (this._app.getActiveNav().canGoBack()){ 
          this._app.getActiveNav().pop();
        };

        // Navigate back on subloaded nav if notified
        if(this._innerNavCtrl && this._innerNavCtrl.canGoBack()){
          this._innerNavCtrl.pop();
        }

      };

      // Fake browser history on each view enter
      this._app.viewDidEnter.subscribe((app) => {
        history.pushState (null, null, "");
      });

    }
  }

Then in my actual loaded page:

ionViewDidEnter() {
    this._events.publish("navController:current", this.navCtrl);
  }

Its messy but working for now until I figure out the nav scoping issue im facing.

@almothafar because the reactions were made with 👍 to remove those annoying +1's ;)

@samvloeberghs but thumb down made even without +1, and it's not only +1 but question too ! it's not StackOverflow ... !

Hello all! This has been solved since beta.11 with our deeplinker. Our deeplinker gives you routing which allows the browser back button to work just like the normal back button on your device. Thanks for using Ionic!

@jgw96 The deeplinker link appears to be broken? Can you verify a solution for this?

I was able to use this to accomplish what I need for now.

import {Component, ViewChild, Injector} from '@angular/core';
import {Platform, MenuController, Nav, App, IonicApp, NavController} from 'ionic-angular';
import {StatusBar} from '@ionic-native/status-bar';
import {SplashScreen} from '@ionic-native/splash-screen';
import {InvitesPage} from "../pages/invites/invites";
import {RewardsPage} from "../pages/rewards/rewards";
import {ConnectionsPage} from "../pages/connections/connections";
import {MessagesPage} from "../pages/messages/messages";
import {ResourcesPage} from "../pages/resources/resources";
import {SignoutPage} from "../pages/signout/signout";
import {DashboardPage} from "../pages/dashboard/dashboard";
import {AccountPage} from "../pages/account/account";
import {HomePage} from "../pages/home/home";
import {TriviaPage} from "../pages/trivia/trivia";
import {Events} from "ionic-angular/util/events";


@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  @ViewChild(Nav) nav: NavController;
  // make HelloIonicPage the root (or first) page

  public rootPage: any; //if logged in, go to dashboard.
  public pages: Array<{title: string, component: any}>;
  public user: any;
  public routeHistory: Array<any>;

  constructor(public platform: Platform,
              public menu: MenuController,
              public statusBar: StatusBar,
              public splashScreen: SplashScreen,
              private _app: App,
              private _ionicApp: IonicApp,
              private _menu: MenuController,
              protected injector: Injector,
              public _events: Events) {

    this.initializeApp();

    // set our app's pages
    this.pages = [
      {title: 'My Account', component: AccountPage},
      {title: 'Dashboard', component: DashboardPage},
      {title: 'Invites', component: InvitesPage},
      {title: 'Rewards', component: RewardsPage},
      {title: 'Connections', component: ConnectionsPage},
      {title: 'Messages', component: MessagesPage},
      {title: 'Resources', component: ResourcesPage},
      {title: 'Trivia', component: TriviaPage},
      {title: 'Sign Out', component: SignoutPage}

    ];

    this.routeHistory = [];
    this.user = {firstName: ''};

  }

  initializeApp() {

    this.platform.ready().then(() => {

      this._setupBrowserBackButtonBehavior();

      let self = this;
      if (sessionStorage.getItem('user')) {
        this.user = JSON.parse(sessionStorage.getItem('user'));
        self.rootPage = TriviaPage;
      } else {
        self.rootPage = HomePage;
      }

      this.routeHistory.push(self.rootPage);
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      this.statusBar.styleDefault();
      this.splashScreen.hide();
    });
  }

  openPage(page) {
    // close the menu when clicking a link from the menu
    this.menu.close();
    // navigate to the new page if it is not the current page
    this.nav.setRoot(page.component);
    //store route history
    this.routeHistory.push(page.component);
  }


  private _setupBrowserBackButtonBehavior() {

    // Register browser back button action(s)
    window.onpopstate = (evt) => {

      // Close menu if open
      if (this._menu.isOpen()) {
        this._menu.close();
        return;
      }

      // Close any active modals or overlays
      let activePortal = this._ionicApp._loadingPortal.getActive() ||
        this._ionicApp._modalPortal.getActive() ||
        this._ionicApp._toastPortal.getActive() ||
        this._ionicApp._overlayPortal.getActive();

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

      if (this.routeHistory.length > 1) {
        this.routeHistory.pop();
        this.nav.setRoot(this.routeHistory[this.routeHistory.length - 1]);
      }


    };

    // Fake browser history on each view enter
    this._app.viewDidEnter.subscribe((app) => {
      if (this.routeHistory.length > 1) {
        history.pushState(null, null, "");
      }

    });

  }
}

@judsonmusic Thanks for posting this. I was able to use this as a foundation and add in a nav controller pop to still maintain the integrity of ionViewCanLeave() implementations on pages and state when possible.

import { Component, ViewChild } from "@angular/core";
import { Nav, Platform, MenuController, IonicApp, App } from "ionic-angular";
import { SplashScreen } from "@ionic-native/splash-screen";
import { StatusBar } from "@ionic-native/status-bar";
import * as moment from "moment";

import { environment } from "../environments/environment";
import { IdleService, RedirectService, SessionService } from "../core";
import { HomePage, LoginPage, PageA, PageB } from "../pages";

@Component({
    templateUrl: "app.html"
})
export class MyApp {
    @ViewChild(Nav) nav: Nav;

    private version = environment.deployDateTime;

    public rootPage: any = LoginPage;
    public pages: Array<{ title: string, component: any }>;
    public routeHistory: Array<any>;

    constructor(
        public menu: MenuController,
        public platform: Platform,
        private ionicApp: IonicApp,
        private idleService: IdleService,
        private redirectService: RedirectService,
        private sessionService: SessionService,
        private splashScreen: SplashScreen,
        private statusBar: StatusBar) {

        this.routeHistory = [];

        this.initializeApp();

        // used for an example of ngFor and navigation
        this.pages = [
            { title: "Home", component: HomePage },
            { title: "Page A", component: PageA },
            { title: "Page B", component: PageB }
        ];

        this.setRootPage();

        this.redirectService.redirectToLogin$.subscribe(() => { this.nav.setRoot(LoginPage); });
    }

    public setRootPage(): void {
        const token: string = this.sessionService.accessToken();
        const tokenExpiration: Date = this.sessionService.expires();

        const currentUtcDate: Date = moment(new Date()).utc().toDate();

        if (token == null || token.length === 0 || tokenExpiration == null || tokenExpiration < currentUtcDate) {
            this.rootPage = LoginPage;
        } else {
            this.rootPage = HomePage;
            this.idleService.startIdleWatch();
        }
    }

    public initializeApp(): void {
        this.platform.ready().then(() => {
            this.setupBrowserBackButtonBehavior();
            this.statusBar.styleDefault();
            this.splashScreen.hide();
        });
    }

    public openPage(page: any): void {
        // reset the content nav to have just this page
        // we wouldn't want the back button to show in this scenario
        this.nav.setRoot(page.component);
    }

    private pushRouteHistory(page: any): void {

        let component: any = page;

        if(page.component) {
            component = page.component;
        }

        if(
            this.routeHistory.length === 0 ||
            this.routeHistory.length > 0 && this.routeHistory[this.routeHistory.length - 1] !== component) {

            this.routeHistory.push(component);
        }
    }

    private setupBrowserBackButtonBehavior(): void {
        window.onpopstate = (event) => {

            console.log("<- Back Button Pressed");

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

            if(this.ionicApp) {
                let activePortal: any =
                    this.ionicApp._loadingPortal.getActive() ||
                    this.ionicApp._modalPortal.getActive() ||
                    this.ionicApp._toastPortal.getActive() ||
                    this.ionicApp._overlayPortal.getActive();

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

            if(this.routeHistory.length > 1) {

                this.routeHistory.pop();

                if(this.nav.canGoBack()) {
                    this.nav.pop().catch((reason) => {
                        console.log("Unable to navigate back:" + reason);
                    });
                } else {
                    this.nav.setRoot(this.routeHistory[this.routeHistory.length - 1]);
                }
            }
        };

        this.nav.viewWillEnter.subscribe((page) => {
            this.pushRouteHistory(page);
        });

        this.nav.viewDidEnter.subscribe((app) => {
            if(this.routeHistory.length > 1) {
                history.pushState(null, null, "");
            }
        });
    }
}

Thanks for the posted solutions, but at the first sight, it seems kind of hacky to me.
Is there some official way to do it recommended by Ionic @jgw96 ?
No offense but the "everything is solved by the deeplinker" (plus the link you provide is now dead) is not very helpful. Is there any official example to follow?
Thanks.

@ddahan looks like they're working on updates to navigation this past release and next. Most likely why documentation disappeared and why they went dark.

https://github.com/ionic-team/ionic/blob/master/CHANGELOG.md#350-2017-06-28

Odd, the link is not present for version 3.x, but it is present for version 2.x:

https://ionicframework.com/docs/2.3.0/api/navigation/DeepLinker/

Page is no longer available. I found some docs about deeplinks here: http://ionicframework.com/docs/native/deeplinks/

I tried the code in this repository https://gist.github.com/t00ts/3542ac4573ffbc73745641fa269326b8
While it actually closes the modals, it makes a mess when I navigate inside tabs, I get a sort of loop.
I ended removing

this._app.viewDidEnter.subscribe((app) => {    
history.pushState (null, null, "");
});

so now the modal closes but the whole page goes back which is still better than the original behaviour where the underneath page goes back and modal remains open!

This is a very misleading behaviour for the user. Probably for now, the only thing to do is to totally avoid modals and keep pushing pages, it would means to rewrite a large part of my PWA...

Is there any official solution, news, work arounds?

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

alan-agius4 picture alan-agius4  ·  3Comments

brandyscarney picture brandyscarney  ·  3Comments

manucorporat picture manucorporat  ·  3Comments

masimplo picture masimplo  ·  3Comments

alexbainbridge picture alexbainbridge  ·  3Comments