angular-ui / ui-router

The de-facto solution to flexible routing with nested views in AngularJS
http://ui-router.github.io/
MIT License
13.53k stars 3k forks source link

Two back button clicks needed to get back when the initial state has got a redirect #3187

Closed NeoGenet1c closed 7 years ago

NeoGenet1c commented 7 years ago

UI router v1.0.0-beta.3 and angular v1.5.9

When an app loads its initial state for the first time and it contains a redirect, the transition graph looks something like following:

State A [redirected] -> State B [success]

However, if a user clicks on the back button, UI router tries to jump back to State A causing another redirect (meaning that nothing happens). In this case, transitions look like this:

State A [redirected] -> State B [ignored]

Finally, another click on the back button will send me back to wherever I was before the application/first state loaded (eg. Google's SERP).

I would expect that a first click on the back button will send a user to a previous page. This issue only occurs if the very first loaded state is a redirect - if I get to a state with a redirect through ui-sref link within my own app, the back button click will send me back right away.

christopherthielen commented 7 years ago

Agreed, that's undesired behavior.

are you using pushstate or hashbang urls?

Would you be able to add {location: 'replace'} to your transition options when doing the initial redirect?

I think this should be solvable by the router. If the initial transition was caused by a url synchronization then when the final state is activated, the url should be replaced.

In the redirect code we could probably enable { location: 'replace' } if the old transition was initiated by a url sync (options.source === 'url') and no prior location: option was provided (by checking the original targetState())

NeoGenet1c commented 7 years ago

I use pushstate URLs ($locationProvider.html5Mode({ enabled: true }).

Can I (somehow) define transition options inside StateDefinition's Object or do I have to use hooks and detect it is first run and first redirect?

christopherthielen commented 7 years ago

Are you using redirectTo?

it would be something like this:

    redirectTo: trans => {
      let $state = trans.router.stateService;
      let firstTrans = trans.$id === 0;
      let options = { location: (firstTrans ? 'replace' : true) };
      return $state.target("home.foo", {}, options);
    }

http://plnkr.co/edit/3arZH4m0iuaUlVBw06So?p=preview

NeoGenet1c commented 7 years ago

Yep, haven't realised that redirectTo can take a callback :) This is what indeed DOES work for me in Angular1:

parentState.redirectTo      = function( $transition ) {
  var $state      = $transition.router.stateService,

  firstTransition = angular.isUndefined( $transition.id ),
  options         = { location: ( firstTransition ? 'replace' : true ) };

  return $state.target( childrenState.name, $transition.params(), options);
}

Btw, Thanks for the great work on new router @christopherthielen!

chaserstrong commented 7 years ago

@NeoGenet1c @christopherthielen Have you solved this issue? I found it may be near these codes:

$rootScope.$watch(function $locationWatch() {
      var oldUrl = trimEmptyHash($browser.url());
      var newUrl = trimEmptyHash($location.absUrl());
      var oldState = $browser.state();
      var currentReplace = $location.$$replace;
      var urlOrStateChanged = oldUrl !== newUrl ||
        ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);

      if (initializing || urlOrStateChanged) {
        initializing = false;

        $rootScope.$evalAsync(function() {
          var newUrl = $location.absUrl();
          var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
              $location.$$state, oldState).defaultPrevented;

          // if the location was changed by a `$locationChangeStart` handler then stop
          // processing this location change
          if ($location.absUrl() !== newUrl) return;

          if (defaultPrevented) {
            $location.$$parse(oldUrl);
            $location.$$state = oldState;
          } else {
            if (urlOrStateChanged) {
              setBrowserUrlWithFallback(newUrl, currentReplace,
                                        oldState === $location.$$state ? null : $location.$$state);
            }
            afterLocationChange(oldUrl, oldState);
          }
        });
      } 

And as urlOrStateChanged is true, the function afterLocationChange is called which makes rul rechange.

NeoGenet1c commented 7 years ago

@chaserstrong Yep. Christopher's solution worked for me.

christopherthielen commented 7 years ago

This plunker demonstrates the issue: http://plnkr.co/edit/3arZH4m0iuaUlVBw06So?p=preview

christopherthielen commented 7 years ago

This will be fixed in rc.1: http://plnkr.co/edit/Xu8FkXNkAFvD6BEJsHvR?p=preview

Note: .otherwise() doesn't use { replace: true } by default, which is another potential issue but would be a breaking change.