Angular routeProvider empties scope on page load - angularjs

I'm trying to integrate Rails with Angular to turn part of my app into a one-page-app using Angular. I have a main module with the following (coffeescript) code for routing:
MainApp.config ($routeProvider) ->
$routeProvider
.when '/',
{
templateUrl: 'post_archive.html'
}
.when '/new',
{
templateUrl: 'new_post.html'
}
.when '/:postSlug',
{
templateUrl: 'show_post.html'
}
.when '/:postSlug/edit',
{
templateUrl: 'edit_post.html'
}
.otherwise
redirectTo: '/'
The main view for this section of the site starts with this haml
%div{ ng_controller: 'PostCtrl', ng_init: 'init()', ng_cloak: true }
And the PostCtrl has this init function:
$s.init = ->
$s.getPost().then ->
$s.getPostList() unless $s.postList
$s.getPreviousPost()
$s.getNextPost()
The idea is, the current post, as well as the next and previous, need to be recalculated, but the postList should remain the same, so it doesn't need to be re-fetched on every page load.
However, it is. It seems that the scope is getting dumped on every page load, which means that it's not really behaving at all like a one-page app, and there's a flicker while the postList reloads, whenever a link is followed within the app.
The links' rendered HTML looks like this, for example:
<a id="link_name" ng_href="#/post-name" class="ng-binding" href="#/post-name">
Post Name
</a>
Any idea what I'm doing wrong here? (Does this have to do with all the pound signs that angular seems to be inserting before the final URL slash?)

This is the expected behavior. If scope.init() is being called on init, it should be called on page load, as the scope will be bound (and initialized) each time its controller route will be accessed.
To avoid that behavior, simply call init() on demand, or — better yet — escalate postList to a higher level in the scope hierarchy (above ng-view, where the route change takes place and re-binds scopes to views), e.g. in the $rootScope. That way its initialization / evaluation won't be tied to the $s scope's init.
To illustrate this [1]:
You can define postList in the topmost scope, — it will be prototypically (is that even a word?) inherited:
$rootScope.postList = [];
It's also sufficient to save it in a parent controller — so long as it's higher in the hierarchy than the router's scope (where ng-view resides), as same rules apply for inheritance with controller's scopes. Something along these lines [2]:
// in the view
%div { ng_contoller: 'ParentCtrl' }
%div { ng_view }
%div{ ng_controller: 'PostCtrl', ng_init: 'init()', ng_cloak: true }
// in ParentCtrl
$s.postList = [];
[1] Caution! not tested!
[2] Risk of bogus code — I don't really know HAML!

Related

Angular $routeChangeSuccess - race condition depending on controller definition?

I'm listening to $routeChangeSuccess inside a controller:
// In MyController
$scope.$on("$routeChangeSuccess", (a, b, c) => {
console.log(a, b, c);
});
This works fine when the route changes as a result of navigation within the app, but not always when going directly to a URL in the browser e.g. /my-new-url
Performance depends on where the controller is defined:
<div ng-controller="MyController"></div>
If this is in the main index.html, it seems to work every time, however:
<mydirective ng-controller="MyController"></mydirective>
with:
app.directive('myDirective', function() {
return {
templateUrl: '<SOME_CONTENT>'
}
});
or indeed:
<mydirective></mydirective>
and :
app.directive('myDirective', function() {
return {
controller: "MyController",
templateUrl: '<SOME_CONTENT>'
}
});
These latter two only hear the $routeChangeSuccess about 50% of the time - presumably due to some compile/digest race condition.
I would much prefer to set my contoller in the directive definition - it's neater, seems nicely modular, and there's no superfluous markup. Unfortunately, it just doesn't work very often.
As a side note: why do I want to do this?
I'm struggling with an app design pattern that Angular seems to refuse to play ball with - I have a number of separate modules within my app, they're all alive at the same time, and I want them to respond to URL changes in an appropriate way.
I don't really understand why this is so hard to achieve. $routeProvider isn't of much use, because I can only have one view/controller per route - that's great if I want to build a Single Page Website, but not if I'm trying to build a Single Page Application with a complex UI. The ui-router plugin isn't really what I'm looking for either - it's more about states with optional routes.
This is the web - the URL is sacred! I want to honour it and use it as much as possible, and I want every component or module in my app to know what it's supposed to do based on the URL. I'm not sure why that's so hard with Angular.
For any component to catch an event, that component must be loaded in the memory when the even is raised.
If you have used ng-view's and controller\directives are defined as partials that load in ng-view, then it is too late for you to subscribe and listen to the event. The controller that loads when partials are loaded inside ng-view is not ready to handle the event by the time it is raised.
Anything outside the ng-view is global in nature and can always subscribe to such events.

AngularJS routing with ng-view relative to HTML page

I am quite new to Angular and now trying to make a simple routing with it. I have my landing page, currently called index2.html, containing some .js and .css includes and a div containing <ng-view></ng-view> where my content should go into.
My app.js looks like this:
var module1 = angular.module('module1', ['ngRoute']);
module1.config(function($routeProvider, $locationProvider){
$routeProvider.when("/",
{
templateUrl: "page1.html",
controller: "uiCtrl"
}
).when("/:param",
{
templateUrl: "page1.html",
controller: "uiCtrl"
}
).when("/transactions",
{
templateUrl: "page2.html",
controller: "uiCtrl"
});
});
But actually this does quite confusing things. Calling http://myurl.com/index2.html, the content of page1.html is properly loaded into the ng-view. So far, so good, but calling index2.html/123 gives my a Not Found instead of interpreting 123 as a parameter. I don't know why, but to make 123 a paremeter i have to call index2.html#123, which works, but then instantly updates the url to index2.html#/123.
Calling index2.html/transactions doesn't work at all. How can i call my /transactions route?
EDIT: If this is useful, i am using JQueryMobile as well in these pages.
I finally got what I want by getting the HTML5 mode work, which makes things so much easier.
After setting $locationProvider.html5Mode(true); I had to re-configure my webserver, what I wasn't able to manage until I found this nice guide: https://github.com/angular-ui/ui-router/wiki/Frequently-Asked-Questions#how-to-configure-your-server-to-work-with-html5mode
Now I can access each of my routes at the conventional way using a slash. Parameters can be passed with ? and &, as usual and I don't need to grapple with confusing hashtags and self-changing url's anymore.

Duplicate controller when using angularjs

I'm usnig AngularJs 1.2.25. I have a route config:
$routeProvider.when(
'/config-root',
{
templateUrl: configRootTemplate,
controller: "ClientConfig",
}
).when(
'/config-action/:action/:_id',
{
templateUrl: configActionTemplate,
controller: "ClientConfigAction",
}
).otherwise({redirectTo: '/'});
In the ClientCOnfig controller, I have a $scope.listConfig variable binding with template for creating, updating list of config.
Everything ok when the first time I visit "config-root" route, I can generate, update... the list.
But when the second time or so far, $scope.listConfig change properly when generate list, update... but the template does not change.
I think when I visit that route again, a new instance of $scope created and does not binding with the template.
How can I fix it?
You can use rootScope to preserve the value for the next time.
change that $scope to $rootScope and access that when ever its required.
If you don't want to pollute the $rootScope you can just store the values in a localStorage as a string and fetch it when ever its required.
// To store the value
localStorage['config'] = JSON.stringify(someObj)
// To fetch the value
var myConfigObj = JSON.parse(localStorage['config'])

AngularJS ui-router $state.go() not triggering state when "views" property is used

I have two parallel views in my AngularJS template. One is the main page view and the second is the navigation bar view. If user is logged in, I display private data in the main view as well as a link to user account in the navigation view. If user is not logged in, I display a welcome screen as well as a login link. So, that's why I'm using two parallel views.
When using ui-router with one ui-view directive in the template, things work as expected. When using two named ui-view directives in my template, $state.go('nameOfState') doesn't work anymore.
Here's a Plunk that's failing in triggering state with $state.go() because it has two views. Here's a Plunk that shows how the same code works when there's only one view.
Why is $state.go() not working?
The problem is the controller for your home state is not being instantiated, meaning the $state.go call is never happening. The controllers are instantiated only on demand. Specifically, the documentation states:
Warning: The controller will not be instantiated if template is not defined.
In order to get mainCtrl to be instantiated, you can add a template to the home state and add an unnamed ui-view to index.html, or you can add a template for one or more of the existing named views (e.g. "main") for the home state and move the mainCtrl to be the controller for those views. E.g. if you replace your existing home state with the following, it should work as expected:
.state('home', {
url: '/',
views: {
'main': {
template: 'main template',
controller: mainCtrl
}
}
})

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