AngularJS Directive that does calculation when scope changes - angularjs

I have an ngRepeat directive that does a count based on the output of filters. This is functioning as expected except when I change the value of the dynamic filter. When I change the filter value, the ngRepeat filters properly but the counts don't always update with it. They do sometimes but not every time. How do I ensure that the value updates every time?
ngRepeat
<li class="list-group-item" data-ng-repeat="e1 in events | availFilter:filterBy | unique:team" ng-init="teamCount = (events | availFilter:filterBy | filter:{team:e1.team})">
<div class="list-group-item-header" data-ng-click="headerClick($event)" >
<span class="title">{{ e1.team }}</span>
<span class="badge">{{ teamCount.length }}</span>'
</div>
Controls that Change Filter
<li role="presentation" data-ng-class="{ active: activeTab('today') }">
<a data-ng-click="filterBy = 'today';">Today</a>
</li>
<li role="presentation" data-ng-class="{ active: activeTab('before8') }"><a data-ng-click="filterBy = 'before8';">Before 8am</a></li>
<li role="presentation" data-ng-class="{ active: activeTab('after5') }"><a data-ng-click="filterBy = 'after5';">After 5pm</a></li>
Filter
availApp.filter('availFilter', function () {
return function (collection, term) {
var outCollection = [];
switch(term){
case 'today':
outCollection = collection;
break;
case 'before8':
angular.forEach(collection, function(item){
if(item.start.getHours() < 8)
outCollection.push(item);
});
break;
case 'after5':
angular.forEach(collection, function(item){
var hr = item.end.getHours();
var min = item.end.getMinutes();
if(hr > 17 || (hr == 17 && min > 0))
outCollection.push(item);
});
break;
default:
break;
}
return outCollection;
};
});

Angular doesn't know when your filter logic changes, that is the fundamental problem here.
The times when it IS updating will be due to some other digest action which is recomputing the filters. The best solution I have for you will be to not be performing this sort of logic in a filter, but instead expose it as a scoped variable which angular IS aware of. E.g. you have a list and you create a subset of this list as a scoped variable, and update that as things change.

Related

Edit an object inside several ng-repeat

I got two ng-repeat who display objects call 'post', and I have a button for edit the text and update it.
Everything works fine but I still got a little problem here's the html code :
<li ng-repeat="post in posts | filter: { etat: 'aTraiter' } ">
<p ng-show="!editing[$index]" ng-model href="#/{{post._id}}">{{ post.corps }}</p>
<input ng-show="editing[$index]" type="text" ng-model="post.corps">
<a ng-show="!editing[$index" ng-click="edit(post)">Editer</a>
<a ng-show="editing[$index]" ng-click="update(post)">Confirmer</a>
<a ng-show="editing[$index]" ng-click="cancel(post)">Annuler</a>
</li>
<li ng-repeat="post in posts | filter: { etat: 'enCours' } ">
<p ng-show="!editing[$index]" ng-model href="#/{{post._id}}">{{ post.corps }}</p>
<input ng-show="editing[$index]" type="text" ng-model="post.corps">
<a ng-show="!editing[$index]" ng-click="edit(post)">Editer</a>
<a ng-show="editing[$index]" ng-click="update(post)">Confirmer</a>
<a ng-show="editing[$index]" ng-click="cancel(post)">Annuler</a>
</li>
the controller :
$scope.editing = [];
$scope.posts= Posts.query();
$scope.edit = function(post){
var idx = $scope.posts.indexOf(post);
$scope.editing[idx] = angular.copy($scope.posts[idx]);
}
$scope.update = function(post){
var idx = $scope.posts.indexOf(post);
Posts.update({id: post._id}, post);
$scope.editing[idx] = false;
}
$scope.cancel = function(post){
var idx = $scope.posts.indexOf(post);
post = angular.copy(post);
$scope.editing[idx] = false;
}
If I got just one post I can edit it and all is ok.
But when I got one post in the both ng-repeat I got some bugs, if I click on edit, buttons change in the both ng-repeat and the both post can be edit.
I'm not really sure but I thinks it's a problem with my :
ng-show="editing[$index]"
I try to put the index of the post like this
ng-show="editing[posts.indexOf(post)]"
But this is not working, can somebody help me ?
(The jsfiddle link)
EDIT the post query :
Posts.query();: Array[0]
0: d
__v: 0
_id: "569563a96a81e64409623179"
corps: "asdad"
etat: "enCours"
nomReseau: "Google+"
section: "evolution"
__proto__: d
1: d
__v: 0
_id: "56954e676a81e6440962316b"
corps: "sdfsdfsf"
etat: "enCours"
nomReseau: "Google+"
section: "evolution"
__proto__: d
Using $index in a filtered repeat to access information in your array.
The problem with using $index, passing it to your controller and then trying to use that index to search for a 'post' in your array of posts, is that $index references your view index and not the true index of the item in the array.
This is traditionally not a problem unless you are filtering your array with ng-repeat. Why? Because $index does not reflex the index of the item, but the index of how the item is appearing in the DOM. So although the first rendered post could have index 5 in your posts array, it will still have $index of 0 because it is the first rendered item in the ng-repeat.
Solution: Separate your data first into two separate arrays and then repeat through them individually.
Controller:
$scope.posts = Posts.query();
$scope.postsATraiter = $scope.posts.filter(function(item, index) {
return item.etat === 'aTraiter';
});
$scope.postsEnCours = $scope.posts.filter(function(item, index) {
return item.etat === 'enCours';
})
$scope.edit = function(post, postType){
var idx = getPostsByType(postType).indexOf(post);
$scope.editing[idx] = angular.copy($scope.posts[idx]);
}
$scope.update = function(post, postType){
var idx = getPostsByType(postType).indexOf(post);
Posts.update({id: post._id}, post);
$scope.editing[idx] = false;
}
$scope.cancel = function(post, postType){
var idx = getPostsByType(postType).indexOf(post);
post = angular.copy(post);
$scope.editing[idx] = false;
}
function getPostsByType(postTypeString) {
if (postTypeString === 'aTraiter') {
return $scope.postsATraiter;
} else {
return $scope.postsEnCours;
}
}
Now that the data is separate you are free to use $index because we know that the $index will respect the true index of the item in the array because it is not being filtered.
<li ng-repeat="post in postsATraiter">
<p ng-show="!editing[$index]" ng-model href="#/{{post._id}}">{{ post.corps }}</p>
<input ng-show="editing[$index]" type="text" ng-model="post.corps">
<a ng-show="!editing[$index" ng-click="edit(post, 'aTraiter')">Editer</a>
<a ng-show="editing[$index]" ng-click="update(post, 'aTraiter')">Confirmer</a>
<a ng-show="editing[$index]" ng-click="cancel(post,'aTraiter')">Annuler</a>
</li>
<li ng-repeat="post in postsEnCours">
<p ng-show="!editing[$index]" ng-model href="#/{{post._id}}">{{ post.corps }}</p>
<input ng-show="editing[$index]" type="text" ng-model="post.corps">
<a ng-show="!editing[$index]" ng-click="edit(post, 'enCours')">Editer</a>
<a ng-show="editing[$index]" ng-click="update(post, 'enCours')">Confirmer</a>
<a ng-show="editing[$index]" ng-click="cancel(post, 'enCours')">Annuler</a>
</li>
You may have to fiddle around with your implementation more, but it appears that $index and the way it is being used may be the root of the problems you are having.
You're using $index, which is just ... well... an index. Both collections can have a value at index==1, right? So, $index isn't unique across the entire set of posts.
Luckily, it would appear that you have an ID for each post that seems to be unique: post._id. How about using that instead?
One little side-note - I'm using jquery's grep method below to find a post by Id. It's fine, but I like underscore.js better. Take a look at both ...
So, here's your controller code:
//This is me willfully and wantonly changing your variables ... sorry.
$scope.selectedPost = undefined;
$scope.selectedPost_unchanged = undefined;
//This is the same, though, so you should feel good ;-)
$scope.posts= Posts.query();
$scope.edit = function(postId){
var result = $.grep($scope.posts, function(p){ return p._id == postId; });
if(result.length==0) { return; }
//just store the one we are editing. that should be cool, right?
$scope.selectedPost = results[0];
//ok... im changing this too... see if you like it better?
//we're gonna use it in the CANCEL method (below).
$scope.selectedPost_unchanged = angular.copy(results[0]);
}
$scope.update = function(post){
Posts.update({id: post._id}, post);
$scope.selectedPost = undefined;
$scope.selectedPost_unchanged = undefined;
}
$scope.cancel = function(post){
post = angular.extend({}, $scope.selectedPost_unchanged);
$scope.selectedPost = undefined;
$scope.selectedPost_unchanged = undefined;
}
//This is new too ... just adding it so that the html is clearer.
$scope.isEditing = function(post) {
if($scope.selectedPost==undefined) { return false; }
return post._id == $scope.selectedPost._id;
}
The HTML changes a bit too
All of your editing[$index] code becomes just isEditing(post)
Voilà! Except maybe use css+ng-class...
Not for nothing, but I would add the while editing/not-editing show/hide using css. Then, add an ng-class to the li element instead (eg - ng-class="{editing: isEditing(post)}"). Then, take care of all your show/hides with css. This way, you only have to put isEditing(post) in ONE location in your html (instead of adding it to every element). ng tags are not expensive, but they REALLY add up inside of ng-repeat tags.

What is the right syntax for ng-show with scope variable

I am setting a ng-show for when I have certain state so my delete button should be shown.
But it's not working. I checked in the console and my method is passing true to the scope variable. Still not sure what is wrong.
application.html
<ul class="nav navbar-nav navbar-right">
<li ng-show="notesState" ><a ng-really-message="Are you sure you want to delete this note?" ng-really-click="deleteNote(note)" class="glyphicon glyphicon-trash " style="float:left;" >Delete note</a></li>
<li><a href="#/home" >+ New Note</a></li>
<li ng-hide="signedIn">Login</li>
<li ng-show="signedIn">{{user.email}}</li>
<li class="pointer" ng-show="signedIn"><a ng-click="logout()">Logout</a></li>
</ul>
in notesCtrl.js
$scope.notesState = $state.is('notes')
is notesState true when the controller is loading? you might need to $scope.$apply() if it something that is false when the controller loads but becomes true later.
sometimes returning a function works but i really do not know why you would need to do this.
$scope.notesState = function(){ return $state.is('notes') }
ng-show="notesState()"
here is $state.is and the function it sometimes returns
$state.is = function is(stateOrName, params, options) {
options = extend({ relative: $state.$current }, options || {});
var state = findState(stateOrName, options.relative);
if (!isDefined(state)) { return undefined; }
if ($state.$current !== state) { return false; }
return params ? equalForKeys(state.params.$$values(params), $stateParams) : true;
};
and
function equalForKeys(a, b, keys) {
if (!keys) {
keys = [];
for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility
}
for (var i=0; i<keys.length; i++) {
var k = keys[i];
if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized
}
return true;
}
if that helps anyone looking into this.
Try using this way.May be it works.
<li ng-show="notesState=='true'" >

View doesn't updates but scope element does

I have this code which is called from the server and receive an object - I do this using signlr and it does enter into this function.
The view consist of 3 tabs and based on the status it should switch the object from one tab to another.
edited:
each tab contains this html list
<ul class="list-group conv_container_base" id="TodayOrder">
<li class="list-group-item" ng-class="{ active : $first}" id="{{order.ID}}" ng-repeat="order in todays | filter: searchval | orderBy: TimeOfPickup">
<a href="#/conv/{{order.ID}}">
{{order.Client_Name}} - {{order.ID}} {{order.Status}}
<h5>{{order.TimeOfPickup | netDate}}</h5>
</a>
</li>
</ul>
padding, pickup and todays are the names of the lists in the scope containing the items.
$scope.$parent.$on("updateStatusRecive", function(e, order) {
$scope.$apply(function() {
if (order.Status == 1) {
var orders = $scope.padding;
$scope.padding = [];
angular.forEach(orders, function(ord) {
if (ord.ID != order.ID) {
$scope.padding.push(ord);
}
});
$scope.pickup.push(order);
} else if (order.Status == 2) {
var orders = $scope.pickup;
$scope.pickup = [];
angular.forEach(orders, function(ord) {
if (ord.ID != order.ID) { //move to the next one
$scope.pickup.push(ord);
}
});
$scope.todays.push(order);
}
});
});
I tried to print out the loops to see what happen, it does change the objects but not the view. I don't have any errors in the console.
What can cause this?

angularjs - filter by multiple models

This seems like it must be simple, I just cannot find the answer.
Let's say I have an array of data, set out like the following:
friends = [{name:'John', age:60, location:'Brighton', street:'Middle Street'},
{name:'Bob', age:5, location:'Brighton', street:'High Street'}];
Now, I want to filter the data based on a text input like so:
<input ng-model="searchText">
<ul>
<li ng-repeat="friend in friends | orderBy:'name' | filter:searchText">
{{friend.name}} - {{friend.location}}</li>
</ul>
This works fine but it filters the input text based on every attribute of the friend object (name, age, location and street). I'd like to be able to filter based on name and location only (ignoring age and street). Is this possible without a custom filter?
Yes, it's possible by simply passing a predicate to the filter instead of a string:
<li ng-repeat="friend in friends | orderBy:'name' | filter:friendContainsSearchText">
$scope.friendContainsSearchText = function(friend) {
return friend.name.indexOf($scope.searchText) >= 0 || friend.location.indexOf($scope.searchText) >= 0
}
Here is how we do it with a custom filter.
DEMO: http://plnkr.co/edit/q7tYjOvFjQHSR0QyGETj?p=preview)
[array] | search:query:columns:operator
> query: this is the term you are looking for
> columns: an array of the names of the properties you want to look for (if empty, will use the angular filter with query)
> operator: a boolean to switch between OR (true) and AND (false, default)
html
<ul>
<li ng-repeat="item in list | search:query:['name','location']:operator">
<pre>{{item | json}}</pre>
</li>
</ul>
js
app.filter('search', function($filter) {
return function(input, term, fields, operator) {
if (!term) {
return input;
}
fields || (fields = []);
if (!fields.length) {
return $filter('filter')(input, term);
}
operator || (operator = false); // true=OR, false=AND
var filtered = [], valid;
angular.forEach(input, function(value, key) {
valid = !operator;
for(var i in fields) {
var index = value[fields[i]].toLowerCase().indexOf(term.toLowerCase());
// OR : found any? valid
if (operator && index >= 0) {
valid = true; break;
}
// AND: not found once? invalid
else if (!operator && index < 0) {
valid = false; break;
}
}
if (valid) {
this.push(value);
}
}, filtered);
return filtered;
};
});
Alternatively you can use:
<li ng-repeat="friend in friends | orderBy:'name' | filter:{ name :searchText}">
You can put several filters just like ....
<div>
<input ng-model="Ctrl.firstName" />
<input ng-model="Ctrl.age" />
<li ng-repeat = "employee in Ctrl.employees | filter:{name:Ctrl.firstName} | filter:{age:Ctrl.age}">{{employee.firstName}}</li>
</div>

angular grouping filter

Following angular.js conditional markup in ng-repeat, I tried to author a custom filter that does grouping. I hit problems regarding object identity and the model being watched for changes, but thought I finally nailed it, as no errors popped in the console anymore.
Turns out I was wrong, because now when I try to combine it with other filters (for pagination) like so
<div ng-repeat="r in blueprints | orderBy:sortPty | startFrom:currentPage*pageSize | limitTo:pageSize | group:3">
<div ng-repeat="b in r">
I get the dreaded "10 $digest() iterations reached. Aborting!" error message again.
Here is my group filter:
filter('group', function() {
return function(input, size) {
if (input.grouped === true) {
return input;
}
var result=[];
var temp = [];
for (var i = 0 ; i < input.length ; i++) {
temp.push(input[i]);
if (i % size === 2) {
result.push(temp);
temp = [];
}
}
if (temp.length > 0) {
result.push(temp);
}
angular.copy(result, input);
input.grouped = true;
return input;
};
}).
Note both the use of angular.copy and the .grouped marker on input, but to no avail :(
I am aware of e.g. "10 $digest() iterations reached. Aborting!" due to filter using angularjs but obviously I did not get it.
Moreover, I guess the grouping logic is a bit naive, but that's another story. Any help would be greatly appreciated, as this is driving me crazy.
It looks like the real problem here is you're altering your input, rather than creating a new variable and outputing that from your filter. This will trigger watches on anything that is watching the variable you've input.
There's really no reason to add a "grouped == true" check in there, because you should have total control over your own filters. But if that's a must for your application, then you'd want to add "grouped == true" to the result of your filter, not the input.
The way filters work is they alter the input and return something different, then the next filter deals with the previous filters result... so your "filtered" check would be mostly irrelavant item in items | filter1 | filter2 | filter3 where filter1 filters items, filter2 filters the result of filter1, and filter3 filters the result of filter 2... if that makes sense.
Here is something I just whipped up. I'm not sure (yet) if it works, but it gives you the basic idea. You'd take an array on one side, and you spit out an array of arrays on the other.
app.filter('group', function(){
return function(items, groupSize) {
var groups = [],
inner;
for(var i = 0; i < items.length; i++) {
if(i % groupSize === 0) {
inner = [];
groups.push(inner);
}
inner.push(items[i]);
}
return groups;
};
});
HTML
<ul ng-repeat="grouping in items | group:3">
<li ng-repeat="item in grouping">{{item}}</li>
</ul>
EDIT
Perhaps it's nicer to see all of those filters in your code, but it looks like it's causing issues because it constantly needs to be re-evaluated on $digest. So I propose you do something like this:
app.controller('MyCtrl', function($scope, $filter) {
$scope.blueprints = [ /* your data */ ];
$scope.currentPage = 0;
$scope.pageSize = 30;
$scope.groupSize = 3;
$scope.sortPty = 'stuff';
//load our filters
var orderBy = $filter('orderBy'),
startFrom = $filter('startFrom'),
limitTo = $filter('limitTo'),
group = $filter('group'); //from the filter above
//a method to apply the filters.
function updateBlueprintDisplay(blueprints) {
var result = orderBy(blueprints, $scope.sortPty);
result = startForm(result, $scope.currentPage * $scope.pageSize);
result = limitTo(result, $scope.pageSize);
result = group(result, 3);
$scope.blueprintDisplay = result;
}
//apply them to the initial value.
updateBlueprintDisplay();
//watch for changes.
$scope.$watch('blueprints', updateBlueprintDisplay);
});
then in your markup:
<ul ng-repeat="grouping in blueprintDisplay">
<li ng-repeat="item in grouping">{{item}}</li>
</ul>
... I'm sure there are typos in there, but that's the basic idea.
EDIT AGAIN: I know you've already accepted this answer, but there is one more way to do this I learned recently that you might like better:
<div ng-repeat="item in groupedItems = (items | group:3 | filter1 | filter2)">
<div ng-repeat="subitem in items.subitems">
{{subitem}}
</div>
</div>
This will create a new property on your $scope called $scope.groupedItems on the fly, which should effectively cache your filtered and grouped results.
Give it a whirl and let me know if it works out for you. If not, I guess the other answer might be better.
Regardless, I'm still seeing the $digest error, which is puzzling: plnkr.co/edit/tHm8uYfjn8EJk3cG31DP – blesh Jan 22 at 17:21
Here is the plunker forked with the fix to the $digest error, using underscore's memoize function: http://underscorejs.org/#memoize.
The issue was that Angular tries to process the filtered collection as a different collection during each iteration. To make sure the return of the filter always returns the same objects, use memoize.
http://en.wikipedia.org/wiki/Memoization
Another example of grouping with underscore: Angular filter works but causes "10 $digest iterations reached"
You can use groupBy filter of angular.filter module,
and do something like this:
usage: (key, value) in collection | groupBy: 'property'or 'propperty.nested'
JS:
$scope.players = [
{name: 'Gene', team: 'alpha'},
{name: 'George', team: 'beta'},
{name: 'Steve', team: 'gamma'},
{name: 'Paula', team: 'beta'},
{name: 'Scruath', team: 'gamma'}
];
HTML:
<ul ng-repeat="(key, value) in players | groupBy: 'team'" >
Group name: {{ key }}
<li ng-repeat="player in value">
player: {{ player.name }}
</li>
</ul>
<!-- result:
Group name: alpha
* player: Gene
Group name: beta
* player: George
* player: Paula
Group name: gamma
* player: Steve
* player: Scruath

Resources