UI-Router's resolve functions are only called once - angularjs

I was going to use ui-routers resolve feature to inject some readiliy resolved promises into my controllers.
I used the example plnkr to make an example.
Consider these nested states: route1 and route1.list.
I have a resolve function called abc defined on route1. Now when I navigate to route1 for the first time, abc is called and will be resolved. Now when I navigate to route1.list and back to route1, abc is not called again.
http://plnkr.co/edit/mi5H5HKVAO3J6PCfKSW3?p=preview
I guess this is intentional, but consider this use-case:
the resolve function retrieves some data via http and should be refreshed/invalidated at some point, maybe on every state change. Is there some way to do this when using nested states?
If the resolve function was called on every inject I could implement it any way I want: Return the old, resolved promise or create a new one.
I have only briefly tested this, but if the states were not nested things would work as I expected. Giving up on nested states because of this stinks though. And having the resolve dependencies available in nested states is actually nice.

Supplying the option reload:true to go() / transtionTo() / ui-sref does the trick :-)
Thanks to Designing the Code for pointing me in this direction. The solution is a bit different though, so I write up this answer.
Reload is documented as follows:
reload (v0.2.5) - {boolean=false}, If true will force transition even if the state or params have not changed, aka a reload of the same state. It differs from reloadOnSearch because you'd use this when you want to force a reload when everything is the same, including search params.
The direct way is to change every ui-sref link into something like this: <a ui-sref="route1" ui-sref-opts="{reload: true}">.
To avoid supplying the option at every link I wrote a decorator around $state (see also http://plnkr.co/edit/mi5H5HKVAO3J6PCfKSW3?p=preview):
myapp.config(function($provide) {
$provide.decorator('$state', function($delegate) {
var originalTransitionTo = $delegate.transitionTo;
$delegate.transitionTo = function(to, toParams, options) {
return originalTransitionTo(to, toParams, angular.extend({
reload: true
}, options));
};
return $delegate;
});
});

Try $state.reload();
It will force resolve the resolves again. Though think there is some issue related to this where controllers aren't reloaded. (ui.router n00b here as well)

Related

Changing how the ui-view directive handles page transitions

Edit: DON'T DO THIS. It's a waste of time unless you want to spend hours debugging, turns out this is a lot more complicated than I first thought. For now the solution to this is:
Move the loading of resources out of the resolvers and into the controller.
Remove all enter animations handled by ui-router.
Add your own animation init and enter classes to the main scope with ng-class.
Use $scope.$emit from the page-specific controllers to tell the main controller when the stuff has finished loading.
In short, if you need this (I have seen a few questions on the ui-router issue tracker) don't use resolvers or ng-animate for the enter animations. You also can't do it on the $stateChangeStart event and the leaving animations as this collides with how ui-router works.
Below is my original question.
I have a specific use case where I need the page transitions and resolves to happen in a certain order currently they happen like this:
resolve > animate out > animate in
I need it more like this:
animate out > resolve > animate in
I decided to check out the ui-router source code and find out why it behaves they way it does. Fortunately it's a very simple mod. In the ui-view directive we have this code.
scope.$on('$stateChangeSuccess', function() {
updateView(false); // cleanupLastView(); is at the end of this function
});
scope.$on('$viewContentLoading', function() {
updateView(false); // cleanupLastView(); is at the end of this function
});
I need to updated it to:
scope.$on('$stateChangeStart', function() {
cleanupLastView();
});
scope.$on('$stateChangeSuccess', function() {
updateView(false); // remove cleanupLastView();
});
scope.$on('$viewContentLoading', function() {
updateView(false); // remove cleanupLastView();
});
The problem is that for obvious reasons, I don't want to go and hack the core. Is there any way to "de-register" the ui-router's ui-view directive and tell it to use one of mine instead?
So I actually wrote this question up and then found an answer. Posting it for anyone else on their travels around the internet.
The answer was to copy BOTH ui-view directives and simply add a new directive called (for example) rich97-view. Then you can use it in your view as if you were using ui-view. The great thing about this methods is that the mod only applies where you need it to, the default behavior is unchanged.

$state.go('state.name') not correctly changing states

I am attempting to move between states (using ui.router) programmatically without the user having to click on anything. The documentation at http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state seems to indicate that I can do this with $state.go('state.name') except that I'm having trouble making this work.
I've created a plunkr to demonstrate my issue. The state should ideally be switching to show "Success" rather than "Failure" when it's run. If you look at the console, it'll say that it cannot read the property 'go' of undefined. How else can I go about changing the state programmatically?
Thanks!
http://plnkr.co/edit/MSLbKaKzmuXPud7ZcKqs
Edit: more clearly specifying that I'm using ui.router
2 things:
You forgot to inject $state into your app.run()
Use $stateChangeSuccess instead
.run(['$rootScope','$state', function($rootScope, $state) {
$rootScope.$on('$stateChangeSuccess', function(event, next) {
$state.go('root.other');
})}]);
Working plunk.

Why can't I access params within resolve?

I have a show page and I want to make sure the data is available before i display this page. It was my understanding that using the resolve property in a route was the best way to do this. However with ui-router i Don't have access to attributes in $state.params. Is there any way around this or am I supposed to just query for the object in the controller? I made a simple plunkr to demonstrate the problem. http://plnkr.co/edit/cSgExy7QOpjx7jEEV7v0?p=preview
As per the documentation, you should be referencing $stateParams in your resolve, not $state.params. Also, the Plunkr was broken because you were injecting a service (Tournament) that you didn't include in your excerpt. This works fine:
resolve: {
tournament: function($stateParams) {
console.log($stateParams) // Prints { tournament_id: 1 }
}
}
It seems that the param object doesn't populate fast enough.
If you wrap the alert in the resolve function with a timeout of 0 milliseconds it seems to work.
I don't have any experience with angular ui router but I assume it is related to the digest cycle still processing and the timeout forces the alert to wait until the digest cycle is complete.

How to run code on a route that has access to services

I wonder what's the best way to configure a route which only purpose is to make use of a service call and then redirect.
I'm currently using this hack:
$routeProvider.when('/talks', {
template: '<div></div>',
controller: ['dateService', '$location', function(dateService, $location){
var nextTalk = dateService.getNextTalkDate();
$location.path('talks/' + nextTalk.format('MM') + '/' + nextTalk.format('YYYY'));
}]
});
Currently I set the controller configuration to an inline controller implementation which has dependencies on the services and then does it's thing and redirects.
However, it feels a bit weird, since I have to set template to some dummy value because otherwise the controller would not be invoked at all.
It feels as if I'm stressing the controller property for something it wasn't intended for.
I guess there should be a way to run some view unrelated code that has access to services on a route. I could right my own $routeProvider I guess but that seems to be a bit heavy for something I would consider should be built in.
Looks like '/talks' is a kinda abstract since its just used to redirect to other routes.. So how about setting up a route like this:
''/talks/:month/:year'
Where :month and :year is optional. If no month or year is given, your service returns a default talk. Which is probably the next talk. If params are given you just fetch the requested data.
So there's no redirect required. Now you specifiy the controller and the needed view and expose your data on the scope. Optionally would it be better to wrap your service call in a promise and resolve it at routeProviders resolve property.
This makes sure that the view only change if everything's resolved fine.
Hope that helps!
There's no way to inject services into anywhere into a route. Whether it be redirectTo or template, any code in there is handled on the module level. This means that the module is loaded first and then when the application has boostrapped itself then the services and injection-level code is executed. So the controller is your best bet (since that does support injection).
Controller is used in a lot of areas in AngularJS and it should work fine for what you're trying to do. You can either handle the redirection like you do in there or you can setup three different routes that point to the same controller (where you build the page).
var handler = { controller : 'Ctrl' };
$routeProvider.when('/talks', handler).
when('/talks/:month, handler).
when('/talks/:month/:year', handler);
And if you do end up using redirection, then just use $location.path(url).replace(). This will make the history stack jump back one level and therefore that the redirection triggering URL won't be in your history.

Combating AngularJS executing controller twice

I understand AngularJS runs through some code twice, sometimes even more, like $watch events, constantly checking model states etc.
However my code:
function MyController($scope, User, local) {
var $scope.User = local.get(); // Get locally save user data
User.get({ id: $scope.User._id.$oid }, function(user) {
$scope.User = new User(user);
local.save($scope.User);
});
//...
Is executed twice, inserting 2 records into my DB. I'm clearly still learning as I've been banging my head against this for ages!
The app router specified navigation to MyController like so:
$routeProvider.when('/',
{ templateUrl: 'pages/home.html',
controller: MyController });
But I also had this in home.html:
<div data-ng-controller="MyController">
This digested the controller twice. Removing the data-ng-controller attribute from the HTML resolved the issue. Alternatively, the controller: property could have been removed from the routing directive.
This problem also appears when using tabbed navigation. For example, app.js might contain:
.state('tab.reports', {
url: '/reports',
views: {
'tab-reports': {
templateUrl: 'templates/tab-reports.html',
controller: 'ReportsCtrl'
}
}
})
The corresponding reports tab HTML might resemble:
<ion-view view-title="Reports">
<ion-content ng-controller="ReportsCtrl">
This will also result in running the controller twice.
AngularJS docs - ngController
Note that you can also attach controllers to the DOM by declaring it
in a route definition via the $route service. A common mistake is to
declare the controller again using ng-controller in the template
itself. This will cause the controller to be attached and executed
twice.
When you use ngRoute with the ng-view directive, the controller gets attached to that dom element by default (or ui-view if you use ui-router). So you will not need to attach it again in the template.
I just went through this, but the issue was different from the accepted answer. I'm really leaving this here for my future self, to include the steps I went through to fix it.
Remove redundant controller declarations
Check trailing slashes in routes
Check for ng-ifs
Check for any unnecessary wrapping ng-view calls (I accidentally had left in an ng-view that was wrapping my actual ng-view. This resulted in three calls to my controllers.)
If you are on Rails, you should remove the turbolinks gem from your application.js file. I wasted a whole day to discover that. Found answer here.
Initializing the app twice with ng-app and with bootstrap. Combating AngularJS executing controller twice
When using $compile on whole element in 'link'-function of directive that also has its own controller defined and uses callbacks of this controller in template via ng-click etc. Found answer here.
Just want to add one more case when controller can init twice (this is actual for angular.js 1.3.1):
<div ng-if="loading">Loading...</div>
<div ng-if="!loading">
<div ng-view></div>
</div>
In this case $route.current will be already set when ng-view will init. That cause double initialization.
To fix it just change ng-if to ng-show/ng-hide and all will work well.
Would like to add for reference:
Double controller code execution can also be caused by referencing the controller in a directive that also runs on the page.
e.g.
return {
restrict: 'A',
controller: 'myController',
link: function ($scope) { ....
When you also have ng-controller="myController" in your HTML
When using angular-ui-router with Angular 1.3+, there was an issue about Rendering views twice on route transition. This resulted in executing controllers twice, too. None of the proposed solutions worked for me.
However, updating angular-ui-router from 0.2.11 to 0.2.13 solved problem for me.
I tore my app and all its dependencies to bits over this issue (details here: AngularJS app initiating twice (tried the usual solutions..))
And in the end, it was all Batarang Chrome plugin's fault.
Resolution in this answer:
I'd strongly recommend the first thing on anyone's list is to disable it per the post before altering code.
If you know your controller is unintentionally executing more than once, try a search through your files for the name of the offending controller, ex: search: MyController through all files. Likely it got copy-pasted in some other html/js file and you forgot to change it when you got to developing or using those partials/controllers. Source: I made this mistake
I had the same problem, in a simple app (with no routing and a simple ng-controller reference) and my controller's constructor did run twice. Finally, I found out that my problem was the following declaration to auto-bootstrap my AngularJS application in my Razor view
<html ng-app="mTest1">
I have also manually bootstrapped it using angular.bootstrap i.e.
angular.bootstrap(document, [this.app.name]);
so removing one of them, it worked for me.
In some cases your directive runs twice when you simply not correct close you directive like this:
<my-directive>Some content<my-directive>
This will run your directive twice.
Also there is another often case when your directive runs twice:
make sure you are not including your directive in your index.html TWICE!
Been scratching my head over this problem with AngularJS 1.4 rc build, then realised none of the above answers was applicable since it was originated from the new router library for Angular 1.4 and Angular 2 at the time of this writing. Therefore, I am dropping a note here for anyone who might be using the new Angular route library.
Basically if a html page contains a ng-viewport directive for loading parts of your app, by clicking on a hyperlink specified in with ng-link would cause the target controller of the associated component to be loaded twice. The subtle difference is that, if the browser has already loaded the target controller, by re-clicking the same hyperlink would only invoke the controller once.
Haven't found a viable workaround yet, though I believe this behaviour is consistent with the observation raised by shaunxu, and hopefully this issue would be resolved in the future build of new route library and along with AngularJS 1.4 releases.
In my case, I found two views using the same controller.
$stateProvider.state('app', {
url: '',
views: {
"viewOne#app": {
controller: 'CtrlOne as CtrlOne',
templateUrl: 'main/one.tpl.html'
},
"viewTwo#app": {
controller: 'CtrlOne as CtrlOne',
templateUrl: 'main/two.tpl.html'
}
}
});
The problem I am encountering might be tangential, but since googling brought me to this question, this might be appropriate. The problem rears its ugly head for me when using UI Router, but only when I attempt to refresh the page with the browser refresh button. The app uses UI Router with a parent abstract state, and then child states off the parent. On the app run() function, there is a $state.go('...child-state...') command. The parent state uses a resolve, and at first I thought perhaps a child controller is executing twice.
Everything is fine before the URL has had the hash appended.
www.someoldwebaddress.org
Then once the url has been modified by UI Router,
www.someoldwebaddress.org#/childstate
...and then when I refresh the page with the browser refresh button, the $stateChangeStart fires twice, and each time points to the childstate.
The resolve on the parent state is what is firing twice.
Perhaps this is a kludge; regardless, this does appear to eliminate the problem for me: in the area of code where $stateProvider is first invoked, first check to see if the window.location.hash is an empty string. If it is, all is good; if it is not, then set the window.location.hash to an empty string. Then it seems the $state only tries to go somewhere once rather than twice.
Also, if you do not want to rely on the app's default run and state.go(...), you can try to capture the hash value and use the hash value to determine the child state you were on just before page refresh, and add a condition to the area in your code where you set the state.go(...).
For those using the ControllerAs syntax, just declare the controller label in the $routeprovider as follows:
$routeprovider
.when('/link', {
templateUrl: 'templateUrl',
controller: 'UploadsController as ctrl'
})
or
$routeprovider
.when('/link', {
templateUrl: 'templateUrl',
controller: 'UploadsController'
controllerAs: 'ctrl'
})
After declaring the $routeprovider, do not supply the controller as in the view. Instead use the label in the view.
In my case it was because of the url pattern I used
my url was like /ui/project/:parameter1/:parameter2.
I didn't need paramerter2 in all cases of state change. In cases where I didn't need the second parameter my url would be like /ui/project/:parameter1/. And so whenever I had a state change I will have my controller refreshed twice.
The solution was to set parameter2 as empty string and do the state change.
I've had this double initialisation happen for a different reason. For some route-transitions in my application I wanted to force scrolling to near the top of the page (e.g. in paginated search results... clicking next should take you to the top of page 2).
I did this by adding a listener to the $rootScope $on $viewContentLoaded which (based on certain conditions) executed
$location.hash('top');
Inadvertently this was causing my routes to be reevaluated and the controllers to be reinitialised
My issue was updating the search parameters like so $location.search('param', key);
you can read more about it here
controller getting called twice due to append params in url
In my case renaming the controller to a different name solved the problem.
There was a conflict of controller names with "angular-ui-tree" module: I renamed my controller from "CatalogerTreeController" to "TreeController" and then this controller starts to be initiated twice on the page where "ui-tree" directive used because this directive uses controller named "TreeController".
I had the same problem and after trying all the answers I finally found that i had a directive in my view that was bound to the same controller.
APP.directive('MyDirective', function() {
return {
restrict: 'AE',
scope: {},
templateUrl: '../views/quiz.html',
controller: 'ShowClassController'
}
});
After removing the directive the controller stopped being called twice. Now my question is, how can use this directive bound to the controller scope without this problem?
I just solved mine, which was actually quite disappointing. Its a ionic hybrid app, I've used ui-router v0.2.13. In my case there is a epub reader (using epub.js) which was continuously reporting "no document found" once I navigate to my books library and select any other book. When I reloaded the browser book was being rendered perfectly but when I selected another book got the same problem again.
My solve was very simple. I just removed reload:true from $state.go("app.reader", { fileName: fn, reload: true }); in my LibraryController
I have the same issue in angular-route#1.6.7, and it because the extra slash in the end of regex route:
.when('/goods/publish/:classId/', option)
to
.when('/goods/publish/:classId', option)
and it works correctly.
Just adding my case here as well:
I was using angular-ui-router with $state.go('new_state', {foo: "foo#bar"})
Once I added encodeURIComponent to the parameter, the problem was gone: $state.go('new_state', {foo: encodeURIComponent("foo#bar")}).
What happened?
The character "#" in the parameter value is not allowed in URLs. As a consequence, angular-ui-router created my controller twice: during first creation it passed the original "foo#bar", during second creation it would pass the encoded version "foo%40bar". Once I explicitly encoded the parameter as shown above, the problem went away.
My issue was really difficult to track down. In the end, the problem was occurring when the web page had missing images. The src was missing a Url. This was happening on an MVC 5 Web Controller. To fix the issue, I included transparent images when no real image is available.
<img alt="" class="logo" src="">
I figured out mine is getting called twice is because i was calling the method twice from my html.
`<form class="form-horizontal" name="x" ng-submit="findX() novalidate >
<input type="text"....>
<input type="text"....>
<input type="text"....>
<button type="submit" class="btn btn-sm btn-primary" ng-click="findX()"
</form>`
The highlighted section was causing findX() to be called twice. Hope it helps someone.

Resources