Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular JS: URL parameter appears before the `#!/` and doesn't appear in $location.search()

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?
like image 280
shrewdbeans Avatar asked May 24 '17 13:05

shrewdbeans


5 Answers

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="[email protected]" 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>
like image 155
adam0101 Avatar answered Nov 14 '22 08:11

adam0101


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.

like image 41
bugs_cena Avatar answered Nov 14 '22 08:11

bugs_cena


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);
like image 44
CodeWarrior Avatar answered Nov 14 '22 09:11

CodeWarrior


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.

like image 26
Preview Avatar answered Nov 14 '22 08:11

Preview


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.

like image 35
Creative Learner Avatar answered Nov 14 '22 08:11

Creative Learner