angularjs loading directive works part of the time - angularjs

In order to standardize loading warnings, and avoid changes in a page when http ajax requests come back and change $scope item values, I set up a simple directive to wrap an element and add "loading" to it.
<loading><div>My content here</div></loading>
This is converted into:
<div><div ng-hide="!ready"><div>My content here</div></div> <div ng-hide="ready">Loading...</div></div>
Pretty straightforward. Then in my controller, I just need to set $scope.ready = true; when everything ajax-y is loaded.
Here is my simple directive:
.directive('loading',function () {
return {
restrict: 'E',
template: '<div><div ng-transclude ng-hide="!ready"></div><div ng-hide="ready">Loading</div></div>',
transclude:true,
replace:true
};
})
Problem is that it works consistently for one page and not the other, even though their layouts are identical.
When examining with firebug, it appears that the one that doesn't work has class="ng-hide" on the first element, as if $scope.ready was never set to true. Of course, both Firebug breakpoints and console logs show that it was set to true.
My suspicion is a scope issue, or possibly how Angular is applying the value of scope changes, but not sure?
UPDATE:
I added messages for when the value of the element gets changed inside the directive, and when it is getting changed in the controller:
// directive
link: function ($scope) {
$scope.$watch('ready',function (newval,oldval) {
console.log("ready changed from "+oldval+" to "+newval);
});
}
// controller
console.log("changing $scope.ready to true");
$scope.ready = true;
For the one that works, I get the following messages:
setting $scope.ready to true
ready changed from undefined to true
For the one that did not, I get the following messages:
setting $scope.ready to true
The directive is not being notified about the change. Is this a scope issue?
HTML:
I really stripped down the html, still managed to recreate the problem:
<!-- THIS ONE HAS THE PROBLEM -->
<loading>
<h3>{{item.name}} Second Page</h3>
</loading>
<!-- THIS ONE IS JUST FINE -->
<loading>
<h3>{{item.name}}</h3>
</loading>
Leads me to believe it must be something in the controller? The routes are basically identical, except for their loading different template html files and different controller names.
UPDATE:
scopes are identical. I had the console messages spit out $scope.$id from inside each controller and from inside the link function (and inside the watch, which is not getting called anyways for the bad one). In all cases, all 3 $id are the same (004) for the page that works, and all 3 $id are the same (006) for the page that does not.
ANOTHER:
setting the directive to isolate scope with scope:{ready:'=ready'} (i.e. 2-way binding has the same effect. Using text binding # makes the whole directive not run correctly.
Apparently, it depends entirely on the ID of the item. I load this via URL /items/:item. If the Item is one that I pre-seeded in the database, so probably has an ID like "40", it all works, both for item type 1 and for item type 2. If it is an auto-generated hex ID for a new item created on the fly, like "8474b2486ee6", then it stumbles and doesn't load.
Finally... if I hit the Back button in the browser, for a second it actually shows!

Related

Why is binding lost when nesting directive in directive?

The Issue
Using my directive within another directive causes certain bindings to be lost, specifically in the ng-repeat usage.
The directive is used in many areas of my application without issue. It renders a list of inputs which is passed to it's scope from a parent template, like so:
<filter-header filters="filters"></filter-header>
Working Scenario
I have used the following scenario throughout the application and not come across an issue to date.
$routeProvider resolves filters list with WebAPI call for a controller
controller assigns list to its own scope, like so: $scope.filters = filters
template uses filter-header element and passes filters from it's scope to the directive, like so: <filter-header filters="filters"></filter-header>
The filter-header directive then renders the filters using an ng-repeat without issue. $$hashKey is present in each item of the filters collection, indicating the binding's existence.
Failing Scenario
In the following scenario, the binding seems to be lost and the ng-repeat fails to render anything.
$routeProvider resolves filters list with WebAPI call for a controller
controller assigns list to its own scope, like so: $scope.filters = filters
template uses a new element directive, assigns filters from it's scope to the new directive's via an attribute.
directive's template uses filter-header element and passes filters from it's scope to the directive, like so: <filter-header filters="filters"></filter-header>
The filter-header directive then FAILS TO render the filters using an ng-repeat. $$hashKey is NOT present in any item of the filters collection.
Annoyingly, I cannot replicate this in Plunker...
Oddity
The directive has another collection of item's passed to it, columns="columns" (can be seen in the code below). Columns binds correctly and is rendered in it's own ng-repeat. I cannot see how Columns is different from Filters as both are used almost exactly the same way.
Looking Deeper...
I have debugged the process all the way. The filters object is getting all the way to the end scope successfully. If I output the contents of filters to the screen within the final directive, using {{ filters }} I can see all of the filters as expected. However, in the very next line where my ng-repeat begins, no filters are iterated through.
To be certain it is not my list causing issues, I used a list that already works using the working scenario mentioned above and the ng-repeat does not render here.
To be certain it is not my directive's code causing issues, I converted it to a controller and routed directly to it (skipping the nested directive) as in the working scenario mentioned above and the ng-repeat now works.
Using $log to inspect the list, I notice one difference. In the working scenario, all lists contain a $$hashKey property for each item in the list. In the failing scenario, the $$hashKey is missing on all items in the list. This seems to indicate that the binding is being lost for some reason.
Can someone tell me the error in my ways? The only real difference I can see in my usage is that I pass the object to an middle-man directive before before passing it on the the directive where it is used. Strangely, in the very same directive, another list is used in a very similar way and it renders without issue within it's ng-repeat, and it's item's all have the $$hashKey property appended.
Code
There's a lot of code involved, so I'll try and pick out the relevant parts.
RouteProvider
$routeProvider.when('/Pride/Admin/AuditForms/:id', {
templateUrl: '/Templates/Admin/editAuditForm.html',
controller: 'editAuditFormController',
resolve: {
sectionFilters: function (auditFormSectionRepository) {
return auditFormSectionRepository.getFilters().$promise;
},
sectionColumns: function (auditFormSectionRepository) {
return auditFormSectionRepository.getColumns().$promise;
}
}
});
EditAuditForm Controller
prideModule.controller("editAuditFormController", function ($scope, sectionFilters, sectionColumns) {
$scope.sectionFilters = sectionFilters;
$scope.sectionColumns = sectionColumns;
});
EditAuditForm Template
<audit-admin-sections audit-form="auditForm" section-filters="sectionFilters" section-columns="sectionColumns" show-deleted="false"></audit-admin-sections>
AuditAdminSections Directive
prideModule.directive('auditAdminSections', function ($log) {
return {
restrict: 'E',
templateUrl: 'templates/admin/auditFormSections.html',
scope: {
sectionFilters: '=',
sectionColumns: '='
},
controller: function ($scope, $route, $timeout, $location, filterLogic, auditFormSectionRepository) {
// do stuff
}
});
AuditFormSections Template
<filter-header filters="sectionFilters" columns="sectionColumns"></filter-header>
FilterHeader Directive
prideModule.directive('filterHeader', function($log) {
return {
restrict: 'E',
templateUrl: 'templates/common/filterHeader.html',
scope: {
filters: '=',
columns: '='
},
controller: function ($scope, filterItemsRepository) {
$log.info("$scope.filters");
$log.info($scope.filters);
// This will log the filters as expected, however the $$hashKey property is missing from the items
}
});
FilterHeader template
<!-- at this point, {{ filters }} produces the list of filters -->
<form class="form-horizontal" ng-repeat="filter in filters">
<!-- at this point, nothing renders -->
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</form>
Update 1
I pulled the code out of the directive and into a new controller to mimic the Working Scenario mentioned above. The ng-repeater now functions as expected and the $$hashKey is present again. So something is definitely related to the difference between route->controller->directive->directive vs route->controller->directive.
Worth mentioning, on top of the code above, there are watches on the filters among other usages.
Update 2: Offender Uncovered
I've nailed it. But it makes no sense yet. It seems as though the form element is to blame. Changing to a div resolves the issue. I'm thinking this may be an angular bug as I'm struggling to see why this could work in one scenario and not the other.
Have discovered two fixes so far, but they are more hacks than fixes as I cannot understand the cause of the original problem. I'm still interested if anyone can point out the real issue, but until then this is the best I have:
Solution 1
After much researching, debugging, head scratching, rewriting, I by luck came across a solution. I'm not happy with it, as it doesn't make sense (unless someone can elaborate for me).
The problem seems to be with the form element and using the ng-repeat attribute on it while being nested in angular directives...!!! This has to be a bug, right?
Solution was as simple as changing this:
<form class="form-horizontal" ng-repeat="filter in filters">
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</form>
to this:
<div class="form-horizontal" ng-repeat="filter in filters">
<label class="col-sm-2 control-label">{{ filter.friendlyName }}</label>
</div>
Solution 2
It seems that the $scope variables were also playing a part in the issue. If I rename each directive's $scope variable name for this particular variable (ie, filters) so that it is unique for each directive, the form ng-repeat works. This makes it seem like there is some sort of conflict within the directives' isolated scopes, however why this is only an issue for form ng-repeat baffles me. As such, it still doesn't explain to me what is the root cause of this behavior.

Angular shows bindings when errors occur. Can this be avoided?

It seems that the default behavior of Angular is to show the bindings that exist in html when an exception is thrown and it can't continue. Is there any way to hide them in this case?
I was thinking that ng-cloak might work for this but I'm trying to avoid adding ng-cloak to each element in my app.
Thoughts?
then add ng-cloak in a large div that contains everything you want to hide. You can even cloak the whole <body>.
Another common solution is to add an isReady variable on your $scope. By default, isReady will be false and you can display your {{...}} with a different value. When everything (like your ajax data) is loaded, set the isReady to true.
For example, (assuming you are injecting $scope to the controller instead of using controller as vm)
in your html markup,
<h1>{{isReady?title:'loading'}}</h1>
in the controller
angular.module('myApp').controller('$scope', function($scope){
activate();
function activate(){
// your code to get ajax data...
$scope.title='title to display';
$scope.isReady=true; // place at the end when everything before runs without error.
}
});   
You can use ngBind instead;
<div ng-bind="someExpression"></div>

Angular UI Bootstrap - Collapsing tab content

Using the AngularJS UI Directives for bootstrap, is there any way to collapse the tab content using the tag?
I have several tabs/pills with content, which will start collapsed (hidden). When any of the tabs is activated, the tab content should collapse open, and stay open until a close button is clicked, which will close the collapsable section.
In the controller, I set $scope.isCollapsed to true. Each of the tabs have the ng-click which calls openCollapse(), which sets isCollapsed to false. If I add the collapse="isCollapsed" directive right to the tag, then the tabs disappear too, not just the content.
How can I fix this?
It took some figuring out, but it is possible!
The main problem I had was with scoping issues and transclusion. I hadn't run into transclusion yet (I'm fairly new to Angular), so I'm still wrapping my head around it a bit.
I tried a few different ways, and a couple others might have worked, if I understood transclusion a bit better, but in the end, this was the simplest for me, and I got it working.
So basically I had to do 4 main things to get this working.
Open up ui.bootstrap-tpls-0.11.0.js (or whichever version # you're using). Do a search for angular.module("template/tabs/tabset.html". In the <div class=\"tab-content\">\n" tag, add collapse=\"isCollapsed\".
The tag (and everything in it) gets replaced when compiled, and this is the code that it is replaced with, so this way we can directly put the collapse directive where we need to.
Also in ui.bootstrap-tpls-0.11.0.js, do a search for .directive('tabset'. Inside the link: function(scope, element, attrs) {, add: scope.isCollapsed = scope.$parent.isCollapsed'
Here we're linking the transcluded scope's isCollapsed to the isCollapsed that is being set in your initial controller (you could also just put initialize isCollapsed in the controller in the next step, instead of just linking it, but I'd already initialized it in my controller and I'd linked it trying to do another method)
Still in ui.bootstrap-tpls-0.11.0.js, do a search for .controller('TabsetController'. Inside this controller, add:
$scope.$on('openCol', function(event){
$scope.isCollapsed = false;
});
$scope.$on('closeCol', function(event){
$scope.isCollapsed = true;
});
What we're doing here is adding event listeners inside the tabset's transcluded scope. What we're going to do at the end, is create an event broadcast. I also added a .$watch(), just so I could see if it was changing:
$scope.$watch('isCollapsed', function(){
console.log("isCollapsed has changed, it is now: " + $scope.isCollapsed);
});
Lastly, in the view's controller (or whichever controller contains the .openCollapse() and .closeCollapse()), change your functions from editing this scopes isCollapsed, to:
$scope.openCollapse = function(){
$rootScope.$broadcast("openCol");
}
$scope.closeCollapse = function($event){
$rootScope.$broadcast("closeCol");
}
This will broadcast the events that are being listened for in the TabsetController. So now the proper scope of the isCollapsed is being changed, and is reflected in the code. Now watch that lovely tab-content collapsing.
Please let me know if I haven't got my terminology quite right, or if there's something inherently wrong with the way I'm doing it. Or, simply, if there are other ways. Always open to suggestions :)

Changes to scope on click are not being updated across my app

Started using Angular last week, read/watched many tutorials and I'm currently trying to build a newsfeed type application.
Here's the skinny: I have a service that gets data from the server. On the newsfeed itself I have two controllers: one that has the entire newsfeed in its scope and another that has an instance for each newsfeed article. If the user clicks an icon on an individual post it should call a service that has been injected into both controllers and then broadcasts a message that the main controller picks up. The main controller then updates a variable in a filter, filtering the newsfeed content based on the user's selection.
Here's the problem: Everything works fine except that the main controller doesn't update the bound variable in the HTML. I have read close to every SO article on two-way binding within an ng-repeat and the related struggles, but in my case the bound variable falls outside an ng-repeat, hence why I'm posting.
The code:
services.factory('filterService', function() {
var filterService = {};
filterService.filterKey = '';
filterService.getFilter = function() {
return filterService.filterKey;
};
filterService.setFilter = function(name) {
filterService.filterKey = name;
$rootScope.$broadcast('changeFilter');
};
return filterService;
});
app.controller('CommentCtrl', function($scope, $timeout, $http, filterService) {
$scope.setSearchParam = function(param) {
alert('clicked: ' + param)
filterService.setFilter(param);
}
app.controller('FeedCtrl', function($scope, articles, filterService, $timeout) {
$scope.articles = articles;
$scope.model = {
value: ''
};
$scope.$on('changeFilter', function() {
console.log(filterService.filterKey);
$scope.model.value = filterService.filterKey
}
});
});
<div class="articles">
<div class="articleStub" ng-repeat="article in articles|filter:model.value">
<div ng-controller="CommentCtrl">
<div class="{{article.sort}}">
<div class="leftBlock">
<a href="#" ng-click="setSearchParam(article.sort)">
<div class="typeIcon">
<i ng-class="{'icon-comments':article.question, 'icon-star':article.create, 'icon-ok-sign':article.notype}"></i>
</div>
</a>
Note: the FeedCtrl controller is called in the app.config $routeprovider function thing whatever its called
Edited to add: the alert and console checks both work, so I'm assuming the issue is not in the filterService or CommentCtrl.
Here's the Plnkr: http://plnkr.co/edit/bTit7m9b04ADwkzWHv88?p=preview
I'm adding another answer as the other is still valid, but is not the only problem!
Having looked at your code, your problems were two fold:
You had a link to href="#"
This was causing the route code to be re-run, and it was creating a new instance of the controller on the same page, but using a different scope. The way I found this out was by adding the debug line: console.log("running controller init code for $scope.$id:" + $scope.$id); into script.js under the line that blanks the model.value. You'll notice it runs on every click, and the $id of the scope is different every time. I don't fully understand what was happening after that, but having two of the same controller looking after the same bit of the page can't be a good thing!
So, with that in mind, I set href="". This ruins the rendering of the button a bit, but it does cure the problem of multiple controllers being instantiated. However, this doesn't fix the problem... what's the other issue?
angular.element.bind('click', ....) is running 'outside the angular world'
This one is a bit more complicated, but basically for angular data-bindings to work, angular needs to know when the scope gets changed. Most of the time it's handled automagically by angular functions (e.g. inside controllers, inside ng-* directives, etc.), but in some cases, when events are triggered from the browser (e.g. XHR, clicks, touches, etc.), you have to tell angular something has changed. You can do this with $scope.$apply(). There are a few good articles on the subject so I'd recommend a bit of reading (try here to begin with).
There are two solutions to this - one is to use the ng-click directive which wraps the native click event with $scope.$apply (and has the added advantage that your markup is more semantic), or the other is to do it yourself. To minimise the changes to your code, I just wrapped your click code in scope.$apply for you:
element.bind('click', function() {
// tell angular that it needs to 'digest' the changes you're about to make.
scope.$apply(function(){
var param = scope.article.sort;
filterService.setFilter(param);
})
});
Here's a working version of your code: http://plnkr.co/edit/X1AK0Bc4NZyChrJEknkN?p=preview
Note I also set up a filter on the list. You could easily ad a button to clear it that is hidden when there's no filter set:
<button ng-click="model.value=''" ng-show="model.value">Clear filter</button>
Hope this helps :)
I actually think the problem is not that your model.value isn't getting updated - all that code looks fine.
I think the problem lies in your filter.
<div class="articleStub" ng-repeat="article in articles|filter:model.value">
This filter will match any object with any field that contains model.value. What you actually want to do is the following:
<div class="articleStub"
ng-repeat="article in articles|filter:{sort: model.value}:true">
To specify that you only want to match against the sort property of each article. The final true parameter means that it'll only allow strict matches as well, so ed wouldn't match edward.
Note that | filter:{sort: model.value}:true is an angular expression, the :s are like JavaScript commas. If you were to imagine it in JavaScript it would be more like: |('filter',{sort:model.value}, true) where | is a special 'inject a filter here' function..
EDIT:
I'm finding it hard to debug your example without having the working code in front of me. If you can make it into a plunker I can help more, but in the meantime, I think you should try to make your code less complicated by using a different approach.
I have created a plunker that shows an easy way to filter a list by the item that you click. I've used very little code so hopefully it's quite easy to understand?
I would also recommend making your feed items into a directive. The directives can have their own controller so it would prevent you having to do the rather ugly repeating of a ng-controller.

AngularJS better way to manage partials and controller data

I've been using directives in AngularJS which build a HTML element with data fetched from the $scope of the controller. I have my controller set a $scope.ready=true variable when it has fetched it's JSON data from the server. This way the directive won't have to build the page over and over each time data is fetched.
Here is the order of events that occur:
The controller page loads a route and fires the controller function.
The page scans the directives and this particular directive is fired.
The directive builds the element and evaluates its expressions and goes forward, but when the directive link function is fired, it waits for the controller to be "ready".
When ready, an inner function is fired which then continues building the partial.
This works, but the code is messy. My question is that is there an easier way to do this? Can I abstract my code so that it gets fired after my controller fires an event? Instead of having to make this onReady inner method.
Here's what it looks like (its works, but it's messy hard to test):
angular.module('App', []).directive('someDirective',function() {
return {
link : function($scope, element, attrs) {
var onReady = function() {
//now lets do the normal stuff
};
var readyKey = 'ready';
if($scope[readyKey] != true) {
$scope.$watch(readyKey, function() {
if($scope[readyKey] == true) {
onReady();
}
});
}
else {
onReady();
}
}
};
});
You could use $scope.$emit in your controller and $rootScope.on("bradcastEventName",...); in your directive. The good point is that directive is decoupled and you can pull it out from project any time. You can reuse same pattern for all directives and other "running" components of your app to respond to this event.
There are two issues that I have discovered:
Having any XHR requests fire in the background will not prevent the template from loading.
There is a difference between having the data be applied to the $scope variable and actually having that data be applied to the bindings of the page (when the $scope is digested). So if you set your data to the scope and then fire an event to inform the partial that the scope is ready then this won't ensure that the data binding for that partial is ready.
So to get around this, then the best solution is to:
Use this plugin to manage the event handling between the controller and any directives below:
https://github.com/yearofmoo/AngularJS-Scope.onReady
Do not put any data into your directive template HTML that you expect the JavaScript function to pickup and use. So if for example you have a link that looks like this:
<a data-user-id="{{ user_id }}" href="/path/to/:user_id/page">My Page</a>
Then the problem is that the directive will have to prepare the :user_id value from the data-user-id attribute, get the href value and replace the data. This means that the directive will have to continuously check the data-user-id attribute to see if it's there (by checking the attrs hash every few moments).
Instead, place a different scope variable directly into the URL
My Page
And then place this in your directive:
$scope.whenReady(function() {
$scope.directive_user_id = $scope.user_id;
});

Resources