Preserve `$location.search()` in angular `redirectTo`?

967 views Asked by At

How can I preserve $location.search() with

$routeProvider.otherwise({redirectTo: xxx})

I know, I can use a function there, which gets the old object $location.search() as the third argument. I mostly care about a single key, let's call it a, and I tried

function(a, b, search) {
    return "/mainpage?a=" + search[a];
}

but it always ends with /mainpage?a=undefined. What's worse, it crashes my debugger, which slows me down. Moreover, this doesn't work for all keys (which I may need later).

3

There are 3 answers

2
alesc On BEST ANSWER

I have made a new approach, where you listen for $routeChangeStart and modify the search() if the previous (non-existing) page contained important GET parameters. Complete source and a demo can be found on this Plunker.

The concept is similar to @Mikko's solution, but the implementation is a bit different (no custom provider) and it works as per author's instructions.

The essence

angular.module('myApp').run(['$rootScope', '$location', 'URL', function($rootScope, $location, URL) {

    // Previous search() value
    var prevSearch = {};

    // Listen to event
    $rootScope.$on('$routeChangeStart', function(evt, next, current) {

        // Have search() parameters and are going to default route
        if(prevSearch !== undefined && $location.path().endsWith(URL.defaultRoute)) {

            // Extract only important params
            var important = {};
            var importantCount = 0;

            for(var param in prevSearch) {

                if(URL.importantParams.indexOf(param) > -1) {

                    important[param] = prevSearch[param];
                    importantCount++;

                }

            }

            // Unset prevSearch
            prevSearch = {};

            // Apply important params, if any
            if(importantCount > 0) {

                $location.search(important);

            }


        } else {

            // Remember current search()
            prevSearch = $location.search();

        }

    });

}]);

The run function sets a listener for $routeChangeStart and also remembers previous values of the search() value (the GET params).

When the app is redirected to the default route, it checks for the previous value of the search() parameters and extracts only the important ones. If any, it overrides the search() value and therefore sets the important GET parameters to the default route.

I have made an URL constant in which one can set the default route and specify the important parameters. The code is the following:

angular.module('myApp').constant('URL', {
    defaultRoute: '/mainpage',
    importantParams: ['a', 'b'],
});

The demo

The first link contains 3 parameters on a non-existing URL. The page is correctly redirected to the default route and only the 2 important ones are preserved.

The second and fourth links doesn't contain any parameters and both links don't apply any additional parameters (regardless on which page you are when you click them).

The third link also contains 3 parameters on the default route and all of them are preserved (no redirecting takes place).

0
alesc On

The main problem is, that in the configuration phase (.config() function), you can only inject providers. And since $location is not a provider, you cannot inject it. Therefore the $location.search() is not working.

You can, however, use the plain JavaScript's window.location.hash in order to obtain the URL parameters. But, this is not the Angular way and it does not work as you would wish.

For example, if you use the following configuration code:

angular.module('myApp').config(['$routeProvider', function($routeProvider) {

    // Return $location.search() equivalent object
    function locationSearch() {

        var search = {};
        var hash = window.location.hash;
        var index = hash.indexOf('?');

        // Has parameters
        if(index > -1) {

            // Extract only the params
            var params = hash.substring(index + 1);

            // Split parameters
            params = params.split('&');

            var tmp;
            for(var i = 0, len = params.length; i < len; i++) {

                tmp = params[i].split('=');
                search[tmp[0]] = tmp[1];

            }

        }

        return search;

    }

    // Generate default URL
    function defaultURL(search) {

        if(search.a === undefined) {

            return '/mainpage';

        } else {

            return '/mainpage?a=' + search.a;

        }

    }

    // Equivalent of $location.search()
    var search = locationSearch();

    // Set up routing
    $routeProvider

        // Events
        .when('/test', {
            templateUrl: 'test.html',
            controller: 'TestCtrl'
        })

        // Default page
        .otherwise({
            redirectTo: defaultURL(search)
        });

}]);

With the above code, you can access the equivalent of $location.search(), and compile a dynamic URL as a default route - BUT the ngRoute escapes it.

For example, the defaultURL() function produces something like this: /mainpage?a=123. However, the ngRoute escapes the special characters (i.e. ? and &) and you get redirected to /mainpage%3Fa=123, which doesn't work as intended.

Further, this only works when you freshly open a page with that URL. When you are already on the page and change to a non-valid URL, the above code doesn't work since the configuration phase is already been done - the .config() fires only once per page load.

TL;DR

To my knowledge, this cannot be done by using the $routeProvider.otherwise() method inside the configuration phase.

1
Mikko Viitala On

You could create your own provider, which you wire up during config and run phases, to preserve your search parameters.

Let's say you have the following:

angular.module('app', ['ngRoute'])
  .provider('myRoute', function myRouteProvider() {
    var defaultUrl;
    var search;

    // get default url with params (config phase)
    this.default = function() {
      var params = '';
      angular.forEach(search, function(value, key) {
        params = params + key + '=' + value + '&';
      });

      if (params.length > 0) {
        params = params.substring(0, params.length - 1);
      }

      var redirectUrl = defaultUrl + (params ? '?' + params : '');
      console.log(redirectUrl);
      return redirectUrl;
    };

    this.$get = function($rootScope) {
      var myRoute = {};

      // set default url (run phase)
      myRoute.setDefault = function(url) {
        if (defaultUrl) {
          throw 'Provider already initialized';
        }

        defaultUrl = url;

        // update params on route change
        $rootScope.$on('$routeChangeStart', function(event, next, current) {
          search = current.params;
        });
      };

      return myRoute;      
    };
  }) 

  .config(function($routeProvider, myRouteProvider) {
    $routeProvider
      .when('/main', {
        templateUrl: 'template.html',
        controller: 'Ctrl'
      })
      .when('/route1', {
        templateUrl: 'template.html',
        controller: 'Ctrl'
      })
      .when('/route2', {
        templateUrl: 'template.html',
        controller: 'Ctrl'
      })
      .otherwise({
        // redirect to default url with latest params
        redirectTo: myRouteProvider.default
      });
  })

  .run(function(myRoute) {
    // set default url (404)
    myRoute.setDefault('/main');
  })

  .controller('Ctrl', function($scope, $location) {
    $scope.$location = $location;
  });

Where index.html contains just

<div>
  <a href="#/main">main</a><br>
  <a href="#/route1">route1</a><br>
  <a href="#/route2?param=abc">route2?param=abc</a><br>
  <a href="#/404">otherwise</a><br>
</div>
<hr>
<div ng-view></div> 

and routes' template.html is simply

<div>
  path: {{ $location.path() }}
  <br>
  params: {{ $location.search() }}
</div>

Now if you navigate between links and hit othwerwise, you'll be redirected to /main with any params from previous route.

imgur


Related plunker here http://plnkr.co/edit/cqR2Nh