Angular $location.path not working as expected - angularjs

I am triggering a URL change using $location.path with parameters, however those parameters are NOT getting passed to the target page.
My source page is sidebar.html, and my target page is dashboard.html.
In my sidebar.html I render a Kendo UI treeview widget as follows :
<span id="treeview" kendo-tree-view="tree"
style="color:white;"
k-template="vm.treeItemTemplate"
k-options="vm.treeOptions"
k-data-source="vm.reportsTree"
k-on-change="vm.onTreeSelect(kendoEvent)">
</span>
and in my sidebar.js I set vm.treeItemTemplate with a click event:
vm.treeItemTemplate = "<a href='\' ng-click='vm.treeItemClick(this)'> {{dataItem.text}} </a>";
then once I click on the treeview item in the browser, I trigger the vm.treeItemClick event to change the URL and eimit the "reptname" parameter:
vm.treeItemClick = function (item) {
console.log("tree view clicked !");
$location.path('/index.html').search({reptname:'MTM'});
}
This is quite painful, and I would appreciate some advice.
I need use the Kendo treeview object in my left nav bar to allow the user to select a variety of report options, which in turn will redirect to the dashboard.html with specific "reptname" parameter values.
Now when I break on $location.path() inside sidebar.js (using Chrome dev tools), I clearly see the correct URL property on the $location object.
$location
LocationHashbangUrl {$$protocol: "http", $$host: "localhost", $$port: 49479, $$parse: function, $$compose: function…}
$$absUrl: "http://localhost:49479/index.html#/index.html?reptname=MTM"
$$host: "localhost"
$$parse: function (url) {
$$path: "/index.html"
$$protocol: "http"
$$replace: false
$$rewrite: function (url) {
$$search: Object
$$url: "/index.html?reptname=MTM"
__proto__: Object
However, once it's REDIRECTED to dashboard.html (which is defined in my routes as url:"/"), I DO NOT see the parameter list in the $location object, nor in the $routeParams object.
This is where I'm stuck !
----- UPDATE -------
When I manually refresh the index.html page with parameters, the $location object contains the parameters.
ex/ URL: Link entered manually
However if I redirect from sidebar.js using $location.path('/index.html').search({reptname:'MTM'}); , I get nothing !

If you see the parameter in the console but not in your executing code, it might be because Chrome tends to evaluate the console.log with a bit of delay, enought for the async requests to finish and have the data populated, therefor it's not a reliable way of debugging.
Can you listen for the $viewContentLoaded event to be fired and then check the path? That is the view is fully loaded and all variables should be set (including $location.path())

I'm not getting any real answers on this issue, so I ended up solving it this way:
In the Source page, sidebar.js, I have defined the treeview's onSelect event() :
function onTreeSelect(e) {
vm.selectedReport = e.sender._current.text();
$location.url('index.html#/?reptname=' + vm.selectedReport);
}
and on the target page, dashboard.js i read the query string using the hash() method:
vm.reptNameParam = $location.$$hash.split("=");
It seems that $location.url() is the ONLY way I can successfully redirect with a parameters list. Using $location.path() does NOT work for me; meaning, I cannot read the query string in my target page using $location.hash() or any other method as it returns an empty string.

Related

How to remove flash of template in angularjs

I am using filters (of web.xml) to check if the session (http session on server side) is valid or not. If the session is not valid, then the response return error 401, and I handle it in angularjs and redirect the route to login page.
So, the situation is like this:
There is a url localhost:8585/xyz. To access this url, login is required.
When the request is made to access this url (without logging in), the filter check whether the session is valid or not. Meanwhile (as the session is being checked), the template of url localhost:8585/xyz is displayed. As the user is not logged in, session is not valid, so filter send 401 error. I handle the error in angularjs and redirect the route to login page.
I don`t want the template to be displayed before getting the actual response from filters and redirecting the route to login page.
When you're defining your routes you can use the resolve property on your route (https://docs.angularjs.org/api/ngRoute/provider/$routeProvider).
This will allow you to specify a function which returns a promise and the route will only be changed once the promise resolves. As an added bonus the value coming back from the resolve promise can be injected into the handling controller.
Here is a plnkr showing how you can use a global function for any of the routes you need.
You can use ng-cloak to hide flash of unstyled content for Angular if need be.
Your main problem seems to happen after Angular is already loaded, though. In the controller that displays the template you can have a setting:
this.loaded = false;
Then in the template itself something like:
<throbber ng-if=!ctrl.loaded/>
<content ng-if=ctrl.loaded>
<!-- template content -->
</content>
Then you can set this.loaded = true when you get the response from the filter. If you get a 401 the redirect happens anyway and this.loaded doesn't get set to true anyway.
Without any example code, it's difficult to know this is possible for you, but the ng-show directive can be used to hide an element if it's provided value is falsy:
<div ng-show="authenticated">
<!-- content you wish to hide here -->
</div>
In your controller, simply set the value to true when the (successful) response is returned
myApp.controller('MyController', function ($scope) {
$scope.init = function() {
// check auth here
$scope.authenticated = true;
}
$scope.init();
});

Browser back button doesn't trigger reload after using Angular's $location.path() update

Situation:
On one of the views of my AngularJS app I have two pagination buttons that use $http to request the next and previous data. I'm using ng-click="getNext(url)" rather than an anchor tag with href="#/items/:itemId". The reason I'm doing this is so that I can quickly page through my content asynchronously w/o triggering a page reload. This works just fine, but using this method bypasses updating the page's URL so its possible to have your current content out of sync with your URL's id (i.e. path is #/items/3 but you're currently viewing item 9). This can be easily fixed by updating the URL in the JS using $location.path('items/' + rsp.id). Now I can fetch data in an async manner and still be able to refresh, bookmark, send links and have the correct/current item display.
Problem:
The problem with this is if a user hits getNext() a few times and then tries to go back using the browser's back button the URL updates like it should but for some reason the browser doesn't perform a refresh–it just sits there and updates the URL. This only occurs with when the item in history is from the same view and I have updated the ID with the location service.
Any ideas would be appreciated.
What I tried so far
Promises + Flags
I've been playing with window.onpopstate, but as of right now I don't have any way to have window.onpopstate differentiate between a browser click and a UI click that updates the URL with $location.path(); Right now it fires the event regardless of the source. So I tried setting a flag to assume that every time this event fires its a browser event, but when its a UI-based event I can disabled that flag because my _myRequestFn() will handle it. Even with this promise setup it still fires the window.onpopstate when _myRequestFn() is fired.
var flag = true; // Assume all requests are browser-based
window.onpopstate = function() {
if (flag) {
$route.reload();
}
console.log('onpopstate fired');
};
_myRequestFn = function(id) {
someService.getMyData(id)
.then(function(rsp) {
// Do a bunch of stuff including...
$location.path('items/' + rsp.id);
return rsp;
})
.then(function() {
// Everything is done reset flag
flag = true;
});
};
$scope.getNext(url) {
flag = false;
_myRequestFn(url);
};
Spoofing
Hitting back through #/item/5 > #/item/4 > #/item/3 just updates the URL and not the path, but if the history has a different param #/thing/2 > #/item/2 that triggers a page refresh. Since the browser back button works if the history is from a different param I wanted to see if I loaded a different param it would work. So I created an #/item-a and #/item-b route that loaded the same template and used the same controllers, just toggled from a/b with each request. I would never recommend this solution to someone, I was more just seeing if I could get the refresh to trigger.
Update
Lots of people on IRC are suggesting that I use UI-Router. I'm really trying to use the out of the box Angular solution. Refactoring my whole routing setup to use UI-Router is not an optimal solution.
app.config(['$locationProvider',
function($locationProvider) {
// Enable html5 mode
$locationProvider.html5Mode(true);
}]);
From the docs:
In HTML5 mode, the $location service getters and setters interact with the browser URL address through the HTML5 history API. This allows for use of regular URL path and search segments, instead of their hashbang equivalents.

$locationChangeStart never broadcasted; $browser.onUrlChange never called after page load

My angular app is non-standard in that we don't use route provider or UI router. WE use a durandal style of navigation that swaps out views using $http.
locationChangeStart never fires I think due to the fact that we are not changing our URL once in the SPA (though we plan to do so in the future sometimes). I can't test this, but if we remain in the SPA and we change the URL to point to a new view thru code (again, not using routeProvider/UiRouter) should I expect this event to fire?
My main issue is that I cannot handle these events: manual url change, back/forward/history buttons, external link clicks
I see $browser#onUrlChange is the function that broadcasts the $locationChangeStart event but that function only gets called on page load for me, but it seems to be exactly the code that should be getting called.
Documentation:
$browser#onUrlChange
It's only called when the url is changed from outside of angular:
user types different url into address bar
user clicks on history (forward/back) button
user clicks on a link
It's not called when url is changed by $browser.url() method
It says this part also:
NOTE: this api is intended for use only by the $location service. Please use the {#link ng.$location $location service} to monitor url changes in angular apps.
I don't know how to 'use $location service to monitory url changes', so I am listening to the $locationChangeStart event:
$rootScope.$on('$locationChangeStart', function(event, next, current) {
// Breakpoint hit here on page load only
var x = 0;
}
Why doesn't the onUrlChange/$locationChangeStart functions run for me?
thanks!

Updating URL in Angular JS without re-rendering view

I'm building a dashboard system in AngularJS and I'm running into an issue with setting the url via $location.path
In our dashboard, we have a bunch of widgets. Each shows a larger maximized view when you click on it. We are trying to setup deep linking to allow users to link to a dashboard with a widget maximized.
Currently, we have 2 routes that look like /dashboard/:dashboardId and /dashboard/:dashboardId/:maximizedWidgetId
When a user maximizes a widget, we update the url using $location.path, but this is causing the view to re-render. Since we have all of the data, we don't want to reload the whole view, we just want to update the URL. Is there a way to set the url without causing the view to re-render?
HTML5Mode is set to true.
In fact, a view will be rendered everytime you change a url. Thats how $routeProvider works in Angular but you can pass maximizeWidgetId as a querystring which does not re-render a view.
App.config(function($routeProvider) {
$routeProvider.when('/dashboard/:dashboardId', {reloadOnSearch: false});
});
When you click a widget to maximize:
Maximum This Widget
or
$location.search('maximizeWidgetId', 1);
The URL in addressbar would change to http://app.com/dashboard/1?maximizeWidgetId=1
You can even watch when search changes in the URL (from one widget to another)
$scope.$on('$routeUpdate', function(scope, next, current) {
// Minimize the current widget and maximize the new one
});
You can set the reloadOnSearch property of $routeProvider to false.
Possible duplicate question : Can you change a path without reloading the controller in AngularJS?
Regards
For those who need change full path() without controllers reload
Here is plugin: https://github.com/anglibs/angular-location-update
Usage:
$location.update_path('/notes/1');
I realize this is an old question, but since it took me a good day and a half to find the answer, so here goes.
You do not need to convert your path into query strings if you use angular-ui-router.
Currently, due to what may be considered as a bug, setting reloadOnSearch: false on a state will result in being able to change the route without reloading the view. The GitHub user lmessinger was even kind enough to provide a demo of it. You can find the link from his comment linked above.
Basically all you need to do is:
Use ui-router instead of ngRoute
In your states, declare the ones you wish with reloadOnSearch: false
In my app, I have an category listing view, from which you can get to another category using a state like this:
$stateProvider.state('articles.list', {
url: '{categorySlug}',
templateUrl: 'partials/article-list.html',
controller: 'ArticleListCtrl',
reloadOnSearch: false
});
That's it. Hope this helps!
We're using Angular UI Router instead of built-in routes for a similar scenario. It doesn't seem to re-instantiate the controller and re-render the entire view.
How I've implemented it:
(my solution mostly for cases when you need to change whole route, not sub-parts)
I have page with menu (menuPage) and data should not be cleaned on navigation (there is a lot of inputs on each page and user will be very very unhappy if data will disappear accidentally).
turn off $routeProvider
in mainPage controller add two divs with custom directive attribute - each directive contains only 'templateUrl' and 'scope: true'
<div ng-show="tab=='tab_name'" data-tab_name-page></div>
mainPage controller contains lines to simulate routing:
if (!$scope.tab && $location.path()) {
$scope.tab = $location.path().substr(1);
}
$scope.setTab = function(tab) {
$scope.tab = tab;
$location.path('/'+tab);
};
That's all. Little bit ugly to have separate directive for each page, but usage of dynamic templateUrl (as function) in directive provokes re-rendering of page (and loosing data of inputs).
If I understood your question right, you want to,
Maximize the widget when the user is on /dashboard/:dashboardId and he maximizes the widget.
You want the user to have the ability to come back to /dashboard/:dashboardId/:maximizedWidgetId and still see the widget maximized.
You can configure only the first route in the routerConfig and use RouteParams to identify if the maximized widget is passed in the params in the controller of this configured route and maximize the one passed as the param. If the user is maximizing it the first time, share the url to this maximized view with the maximizedWidgetId on the UI.
As long as you use $location(which is just a wrapper over native location object) to update the path it will refresh the view.
I have an idea to use
window.history.replaceState('Object', 'Title', '/new-url');
If you do this and a digest cycle happens it will completely mangle things up. However if you set it back to the correct url that angular expects it's ok. So in theory you could store the correct url that angular expects and reset it just before you know a digest fires.
I've not tested this though.
Below code will let you change url without redirection such as: http://localhost/#/691?foo?bar?blabla
for(var i=0;i<=1000;i++) $routeProvider.when('/'+i, {templateUrl: "tabPages/"+i+".html",reloadOnSearch: false});
But when you change to http://localhost/#/692, you will be redirected.

AngularJS Paging with $location.path but no ngView reload

My single page application loads a home page and I want to display a series of ideas. Each of the ideas is displayed in an animated flash container, with animations displayed to cycle between the ideas.
Ideas are loaded using $http:
$scope.flash = new FlashInterface scope:$scope,location:$location
$http.get("/competition.json")
.success (data) ->
$scope.flash._init data
However, to benefit from history navigation and UX I wish to update the address bar to display the correct url for each idea using $location:
$location.path "/i/#{idea.code}"
$scope.$apply()
I am calling $apply here because this event comes from outwith the AngularJS context ie Flash. I would like for the current controller/view to remain and for the view to not reload. This is very bad because reloading the view results in the whole flash object being thrown away and the preloader cycle beginning again.
I've tried listening for $routeChangeStart to do a preventDefault:
$scope.$on "$routeChangeStart", (ev,next,current) ->
ev.preventDefault()
$scope.$on "$routeChangeSuccess", (ev,current) ->
ev.preventDefault()
but to no avail. The whole thing would be hunky dory if I could figure out a way of overriding the view reload when I change the $location.path.
I'm still very much feeling my way around AngularJS so I'd be glad of any pointers on how to structure the app to achieve my goal!
Instead of updating the path, just update query param with a page number.
set your route to ignore query param changes:
....
$routeProvider.when('/foo', {..., reloadOnSearch: false})
....
and in your app update $location with:
...
$location.search('page', pageNumber);
...
From this blog post:
by default all location changes go through the routing process, which
updates the angular view.
There’s a simple way to short-circuit this, however. Angular watches
for a location change (whether it’s accomplished through typing in the
location bar, clicking a link or setting the location through
$location.path()). When it senses this change, it broadcasts an
event, $locationChangeSuccess, and begins the routing process. What
we do is capture the event and reset the route to what it was
previously.
function MyCtrl($route, $scope) {
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function(event) {
$route.current = lastRoute;
});
}
My solution was to use the $routeChangeStart because that gives you the "next" and "last" routes, you can compare them without the need of an extra variable like on $locationChangeSuccess.
The benefit is being able to access the "params" property on both "next" and "last" routes like next.params.yourproperty when you are using the "/property/value" URL style and of course use $location.url or $location.path to change the URL instead of $location.search() that depends on "?property=value" URL style.
In my case I used it not only for that but also to prevent the route to change is the controller did not change:
$scope.$on('$routeChangeStart',function(e,next,last){
if(next.$$route.controller === last.$$route.controller){
e.preventDefault();
$route.current = last.$$route;
//do whatever you want in here!
}
});
Personally I feel like AngularJS should provide a way to control it, right now they assume that whenever you change the browser's location you want to change the route.
You should be loading $location via Dependency Injection and using the following:
$scope.apply(function () {
$location.path("yourPath");
}
Keep in mind that you should not use hashtags(#) while using $location.path. This is for compability for HTML5 mode.
The $locationChangeSuccess event is a bit of a brute force approach, but I found that checking the path allows us to avoid page reloads when the route path template is unchanged, but reloads the page when switching to a different route template:
var lastRoute = $route.current;
$scope.$on('$locationChangeSuccess', function (event) {
if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
$route.current = lastRoute;
}
});
Adding that code to a particular controller makes the reloading more intelligent.
Edit: While this makes it a bit easier, I ultimately didn't like the complexity of the code I was writing to keep friendly looking URL's. In the end, I just switched to a search parameter and angular handles it much better.
I needed to do this but after fussing around trying to get the $locationChange~ events to get it to work I learned that you can actually do this on the route using resolve.
$routeProvider.when(
'/page',
{
templateUrl : 'partial.html',
controller : 'PageCtrl',
resolve : {
load : ['$q', function($q) {
var defer = $q.defer();
if (/*you only changed the idea thingo*/)
//dont reload the view
defer.reject('');
//otherwise, load the view
else
defer.resolve();
return defer.promise;
}]
}
}
);
With AngularJS V1.7.1, $route adds support for the reloadOnUrl configuration option.
If route /foo/:id has reloadOnUrl = false set, then moving from /foo/id1 to /foo/id2 only broadcasts a $routeUpdate event, and does not reload the view and re-instantiate the controller.

Resources