6

I'm integrating a small existing Angular JS app made by someone else into an exiting website that was not built with Angular.

All the Angular JS app files are kept in a completely separate directory and then the user can click on a link to take them to the index of that sub directory. For example:

<a href="/path/to/angular-app/?returnUrl=/original/location">Login.</a>

As you can see above, I need to pass a URL parameter called returnUrl. I then need to pick up this value in the Angular JS app, using:

$location.search()

However, this actually returns an empty object. I think it's because the Angular JS is appending #!/login on the URL after the URL parameter, like so:

example.com/path/to/app?returnUrl=/original/location/#!/login

If I modify the URL in the browser, moving the returnUrl parameter to the end of the URL and refresh the page, it works and $location.search() returns {returnUrl: "/original/location/"}. However, as far as I can see I'm not able to change this in my website because the parameter is part of the link to the Angular JS app and #!/login is added afterwards automatically.

I'm new AngularJS, so please explain as clearly as you can:

  1. What is going on here? And why is the URL parameter before the #!/login?
  2. Why does this prevent $location.search() from returning anything? If I move the param to the end of the resulting URL, it works.
  3. What can I do to fix this?
Preview
  • 35,317
  • 10
  • 92
  • 112
shrewdbeans
  • 11,971
  • 23
  • 69
  • 115
  • [**This answer**](https://stackoverflow.com/questions/41226122/url-hash-bang-prefix-instead-of-simple-hash-in-angular-1-6/41226167#41226167) will explain your issue about the `#!` happening in your URL. – Mistalis May 24 '17 at 14:50

6 Answers6

4

You just need to use window.location.search

> window.location.search
> "?returnUrl=/original/location/"

$location.search() gives you values only if your route has defined it.

bugs_cena
  • 495
  • 5
  • 11
4

What is going on here? And why is the URL parameter before the #!/login?

You can think of querystring parameters before the #! as a server-side parameters, and querystring parameters after the #! as client-side parameters. This is perfectly valid:

http://example.com?serverParam=client1#!angularRoute?angularParam=someValue

The server-side parameters allow you to configure how the page is served from the server and then, once server page is served and the Angular app is loaded, use the client-side parameters to supply what that particular Angular app is expecting.

Why does this prevent $location.search() from returning anything? If I move the param to the end of the resulting URL, it works.

Because nothing was specified for the client-side parameters until you moved the values there.

What can I do to fix this?

  1. You could change the links to be like how you modified them to get them to work: <a href="example.com/path/to/app/#!/login?returnUrl=/original/location">Login.</a> or duplicate the parameter so it's available on both the server and the client <a href="example.com/path/to/app/?returnUrl=/original/location#!/login?returnUrl=/original/location">Login.</a>
  2. You can inject $window and get the server-side values from $window.location.search
  3. You can inject $location and use $location.absUrl()

Here is an example of how you can set the client-side parameters from the server-side parameters:

var app = angular.module('YOURAPPNAME', []);

app.run(['$location', function($location) {
  // I'm going to fake out the url so it runs in the SnippetEditor.
  var url = "example.com/path/to/app?returnUrl=/original/location/#!/login"; //$location.absUrl();
  var hashbangIdx = url.indexOf('#!');
  var serverStr = hashbangIdx > -1 ? url.substring(0, hashbangIdx) : url;
  var qIdx = serverStr.indexOf("?");
  var p = qIdx > -1 ? serverStr.substring(qIdx).split(/[&||?]/) : [];
  for (var i = 0; i < p.length; i += 1) {
    if (p[i].indexOf('=') > -1) {
      var param = p[i].split('=');
      $location.search(param[0], param[1]);
    }
  }
}]);

app.controller('MainCtrl', ['$scope', '$location', function($scope, $location) {
  $scope.params = $location.search();
}]);
<!DOCTYPE html>
<html ng-app="YOURAPPNAME">

<head>
  <meta charset="utf-8" />
  <title>AngularJS</title>
  <script data-require="angular.js@1.5.x" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.11/angular.min.js" data-semver="1.5.11"></script>
  <script src="app.js"></script>
</head>

<body ng-controller="MainCtrl">
  <h4>Parameters:</h4>
  <p ng-repeat="(name, val) in params">
    {{name}}: {{val}}
  </p>
</body>

</html>
adam0101
  • 29,096
  • 21
  • 96
  • 174
  • I like your answer, however I can't seem to get your example to work, could you simplify it a little? There are a few examples in other answers where getting the param from the URL seems a little more straightforward, although I like that your example seems to be redirecting. I can't seem to get it work though! – shrewdbeans May 31 '17 at 13:15
1

Here are the answers to your questions:

1) AngularJS appends the # at the end of the url at which the page was loaded. So, if the url to load the page already had the query parameter of returnUrl, which seems to be the case, it will always show up to the left of the #.

2) An AngularJS application, is generally only concerned about what occurs after the # in the url. AngularJS uses this for routing and passing query parameters between states and more similar stuff. The $location.search() is meant to retrieve all the query parameters after the #.

3) Here is how you can extract the query parameter that occurs before the #

function getParamFromURL (paramName) {
    var searchStr = window.location.search.substring(1);
    var keyValues = searchStr.split('&');
    for(var i=0; i<keyValues.length; i++) {
        var split = keyValues[i].split('=');
        if(split[0] === paramName) {
            return decodeURIComponent(split[1]);
        }
    }
}

var value = getParamFromURL('returnUrl');
console.log(value);
CodeWarrior
  • 2,721
  • 3
  • 18
  • 18
1

The # you're seeing in the url is actually what's called an anchor link, or fragment. It's usually done to jump to a specific element by its id, for example navigating clicking on a <a href="#footer"> will directly scroll your page if you have an element matching this id.

Angular and other frameworks, like React, make use of this functionality to create what's called a hash routing by using the hash as the current active route. The advantages of doing so is that it will allow for a very quick setup and it will work out-of-the-box on non-HTML5 browsers.

Regarding why the query parameter is before the hash, it's only because that's actually what you specified in your link. Angular considers the url you are loading as the root of the project and adds its own # with the current router after its initialization. You could potentially have a server that serves the Angular app only while specifying this query parameter, thus the importance of keeping it in place. If the query param is place after the hash, it's considered as part of the Angular app routing, thus returned when you call $location.search().

What I would advice you to do is either explicitly specifying the # in your link, or use html5 mode. Html5 mode means that you won't get the hash form in your url, rather "normal" ones that are less ugly.

To do that, you'll have to enable html5 mode in your app,

.config(function ($locationProvider) {
  $locationProvider.html5Mode(true);
}

And add the meta base in your head

<base href="/" />

The thing is that now, if you try to load a page that is not the home, you'll get into a 404 issue.

To prevent that, you'll have to serve your Angular entry for each subroute of your application. There is multiple ways, depending on what you use/ want to use on the server. Here is the Angular FAQ that showcases most of them.

Preview
  • 35,317
  • 10
  • 92
  • 112
1

You can use $location.absUrl() : It will return full URL with all segments encoded according to rules specified in RFC 3986.

var resultURl = GetURLParameter('returnUrl');
console.log(resultURl);

function GetURLParameter(param) {
    // var URL =  $location.absUrl();
    var URL = "example.com/path/to/app?returnUrl=/original/location/#!/login";
    var allVariables = URL.split('?');
    for (var i = 0; i < allVariables.length; i++) {
        var paramName = allVariables[i].split('=');
        if (paramName[0] == param) {
            return paramName[1].split("#")[0];
        }
    }
}

============================== OR ====================================

RouteProvider and routeParams will work in your situation.

You can pass the returnUrl as a route param in the routing and get back into the controller using $routeParams.returnUrl.

What can I do to fix this?

You can try this approach to fix this.

HTML :

<a href="#login/original/location">Login Return Url</a>

Routing file :

$routeProvider.when('/login/:returnUrl', {
    templateUrl: 'partials/partial.html',    
    controller: 'MyCtrl'
});

In controller, inject $routeParams :

.controller('MyCtrl', ['$scope','$routeParams', function($scope, $routeParams) {
  var url = $routeParams.returnUrl;
}]);

Note :

If you're using ngRoute, you can inject $routeParams into your controller.

If you're using angular-ui-router, you can inject $stateParams into your controller.

Debug Diva
  • 26,058
  • 13
  • 70
  • 123
-1

Check this,

function getValue(key, url) {
    var regex = new RegExp("[?&]" + key + "(=([^&#]*)|&|#|$)"),
        results = regex.exec(url);
    return results[2];
}
var value = getValue("returnUrl","example.com/path/to/app?returnUrl=/original/location/#!/login");
console.log(value);
S B
  • 1,363
  • 12
  • 21