61

I have built an app that uses router 3.0.0-beta.1 to switch between app sections. I also use location.go() to emulate the switch between subsections of the same page. I used <base href="/"> and a few URL rewrite rules in order to redirect all routes to index.html in case of page refresh. This allows the router to receive the requested subsection as a URL param. Basically I have managed to avoid using the HashLocationStrategy.

routes.ts

export const routes: RouterConfig = [
    {
        path: '',
        redirectTo: '/catalog',
        pathMatch: 'full'
    },
    {
        path: 'catalog',
        component: CatalogComponent
    },
    {
        path: 'catalog/:topCategory',
        component: CatalogComponent
    },
    {
        path: 'summary',
        component: SummaryComponent
    }
];

If I click on a subsection in the navigation bar 2 things happen:

  • logation.go() updates the URL with the necessary string in order to indicate the current subsection
  • A custom scrollTo() animation scrolls the page at the top of the requested subsection.

If I refresh the page I am using the previously defined route and extract the necessary parameter to restore scroll to the requested subsection.

this._activatedRoute.params
    .map(params => params['topCategory'])
    .subscribe(topCategory => {
        if (typeof topCategory !== 'undefined' &&
            topCategory !== null
        ) {
            self.UiState.startArrowWasDismised = true;
            self.UiState.selectedTopCategory = topCategory;
        }
    });

All works fine except when I click the back button. If previous page was a different section, the app router behaves as expected. However if the previous page/url was a subsection, the url changes to the previous one, but nothing happens in the UI. How can I detect if the back button was pressed in order to invoke the scrollTo() function to do it's job again?

Most answers I saw relly on the event onhashchange, but this event does not get fired in my app since I have no hash in the URL afterall...

Adrian Moisa
  • 3,923
  • 7
  • 41
  • 66

11 Answers11

84

I don't know if the other answers are dated, but neither of them worked well for me in Angular 7. What I did was add an Angular event listener by importing it into my component:

import { HostListener } from '@angular/core';

and then listening for popstate on the window object (as Adrian recommended):

  @HostListener('window:popstate', ['$event'])
  onPopState(event) {
    console.log('Back button pressed');
  }

This worked for me.

VSO
  • 11,546
  • 25
  • 99
  • 187
  • 1
    Would this work if I'm using mobile device and pull down the screen and refresh the page? – indrajeet Jul 20 '19 at 05:39
  • 5
    As MDN states: "A popstate event is dispatched to the window each time the active history entry changes between two history entries for the same document". That means the event will be triggered when the user clicks on "back" but also on "forward" – Florent Arlandis Sep 03 '19 at 14:16
  • @indrajeet /* Handle the tab closed/refreshed event */ @HostListener('window:beforeunload', ['$event']) unloadNotification($event: any) { if (pendingTask()) { /* Implement your logic that either you have any pending task or not */ return false; } else { return false; } } – Muhammad Bilal Feb 13 '20 at 03:40
  • "Latest" Angular version doesn't help people who come to the question years later. Would love to have this have stated the actual version you were on. – Sean Halls May 24 '20 at 06:33
  • 1
    @SeanHalls We were on 7 at the time. I will update the answer. – VSO May 26 '20 at 14:47
  • Beware the state object in the PopStateEvent will have "navigationId" and not your state data you provided. – Raid Jul 14 '22 at 22:09
54

Another alternative for this issue would be to subscribe to the events emitted by the Angular Router service. Since we are dealing with routing, it seems to me that using Router events makes more sense.

constructor(router: Router) {
    router.events
      .subscribe((event: NavigationStart) => {
        if (event.navigationTrigger === 'popstate') {
          // Perform actions
        }
      });
}

I would like to note that popstate happens when pressing back and forward on the browser. So in order to do this efficiently, you would have to find a way to determine which one is occurring. For me, that was just using the event object of type NavigationStart which gives information about where the user is coming from and where they are going to.

Jon
  • 2,456
  • 21
  • 28
13

To detect browser back button click import platformlocation from '@angular/common and place the below code in your constructor :

 constructor(location: PlatformLocation) {
     location.onPopState(() => {
        alert(window.location);
     }); }
Shivakumar NH
  • 161
  • 1
  • 12
  • 8
    According to Angular's official documentation, PlatformLocation should not be used directly by an application developer. – KlavierCat May 03 '19 at 10:43
12

This is the latest update for Angular 13

You have to first import NavigationStart from the angular router

import { NavigationStart, Router } from '@angular/router';

Then add the following code to the constructor

constructor(private router: Router) {
  router.events.forEach((event) => {
    if(event instanceof NavigationStart) {
      if (event.navigationTrigger === 'popstate') {
        /* Do something here */
      }
    }
  });
}
Nikhil Yadav
  • 1,419
  • 1
  • 13
  • 8
  • Doesn't work for mobile Chrome. Works for desktop Chrome – Ste Oct 21 '22 at 09:00
  • but this will be executed only once when the module is loaded, and not every time a back button is pressed, right? UPDATE: for some reason it works, but I don't see why. As if the forEach function doesn't just perform a loop, but also a subscribe(), which seems kind of hacky to me. – LachoTomov Jan 02 '23 at 13:42
9

Angular documentation states directly in PlatformLocation class...

  • This class should not be used directly by an application developer.

I used LocationStrategy in the constructor

constructor(location: LocationStrategy) {
  location.onPopState(() => {
    alert(window.location);
  });
}
pbialy
  • 1,025
  • 14
  • 26
Rmalmoe
  • 993
  • 11
  • 13
9

A great clean way is to import 'fromEvent' from rxjs and use it this way.

fromEvent(window, 'popstate')
  .subscribe((e) => {
    console.log(e, 'back button');
  });
Ryann Galea
  • 307
  • 3
  • 18
  • 2
    I adopted this solution, much cleaner indeed. I define a subscription, then do the `subscribe` in `ngOnInit`, and the `unsubscribe` in the `ngOnDestroy`. – KlavierCat May 03 '19 at 12:29
3

Using onpopstate event did the trick:

window.addEventListener('popstate',
    // Add your callback here
    () => self.events.scrollToTopCategory.emit({ categId: self.state.selectedTopCategory })
);
Adrian Moisa
  • 3,923
  • 7
  • 41
  • 66
3

I agree with Adrian Moisa answer,

but you can use "more Angular 2 way" using class PlatformLocation by injecting to your component or service, then you can define onPopState callback this way:

this.location.onPopState(()=>{
  // your code here...
  this.logger.debug('onpopstate event');
});
vanrado
  • 348
  • 3
  • 13
2

Simpler way - Link

import { PlatformLocation } from '@angular/common';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
...

constructor(
private platformLocation: PlatformLocation ,
private modalService: NgbModal
)
{
platformLocation.onPopState(() => this.modalService.dismissAll());
}
Nikheel
  • 215
  • 4
  • 7
0

I honestly don't know your use case. And the thread is quite old. But this was the first hit on Google. And if someone else is looking for this with Angular 2 and ui-router (just as you are using).

It's not technically detecting the back button. It's more detecting whether you as a developer triggered the state change or whether the user updated the URL themselves.

You can add custom options to state changes, this can be done via uiSref and via stateService.go. In your transitions, you can check whether this option is set. (It won't be set on back button clicks and such).

Using ui-sref

    <a uiSref="destination-name" [uiOptions]="{custom: {viaSref: true}}">Bar</a>

Using state.go

import {StateService} from '@uirouter/core';

...

@Component(...)
export class MyComponent {

    constructor(
        private stateService: StateService
    ) {}

    public myAction(): void {
        this.stateService.go('destination-name', {}, {custom: {viaGo: true}});
    }
}

You can detect it in any transition hook, for example onSucces!

import {Transition, TransitionOptions, TransitionService} from '@uirouter/core';

...

@Component(...)
export class MyComponent implements OnInit {

    constructor(
        private transitionService: TransitionService
    ) {}

    public ngOnInit(): void {
        this.transitionService.onSuccess({}, (transition: Transition) => this.handleTransition(Transition));
    }

    private handleTransition(transition: Transition): void {
        let transitionOptions: TransitionOptions = transition.options();
        if (transitionOptions.custom?.viaSref) {
            console.log('viaSref!');
            return;
        }
        if (transitionOptions.custom?.viaGo) {
            console.log('viaGo!');
            return;
        }
        console.log('User transition?');
    }
}
Samantha Adrichem
  • 833
  • 1
  • 12
  • 23
-2

You can check the size of "window.history", if the size is 1 you can't go back.

isCanGoBack(){
    window.history.length > 1;
}
pilladooo
  • 713
  • 6
  • 10