View doesn't updates but scope element does - angularjs

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?

Related

AngularJS Directive that does calculation when scope changes

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.

AngularJS cant get category "All" while filtering

I started to play with angular and I am trying to write a simple app that consists of categories containing items. ( I am trying to implement a tutorial for my needs )
Now I am trying to add a filter to select items by categories. I can filter them unless I choose All categories. I cant get all the categories.
I have edges service :
angular.module('swFrontApp')
.controller('EdgesController', function ($scope, edges,categories) {
$scope.edges = edges.query();
$scope.categories = categories.query();
$scope.filterBy = {
search: '',
category: $scope.categories[0]
};
var selectedEdge = null;
$scope.selectEdge = function(edge) {
selectedEdge = (selectedEdge === edge) ? null : edge;
};
$scope.isSelected = function(edge) {
return edge === selectedEdge;
};
$scope.displayRequirements = function(reqs) {
var result = '';
for ( var i = 0; i < reqs.length; i ++) {
if (result !== '' ) { result += ', '}
if (reqs[i].name) {
result += reqs[i].name+ ' ';
}
result += reqs[i].value;
}
return result;
};
});
and I try to filter them using :
angular.module('swFrontApp').filter('edges', function() {
return function(edges, filterBy) {
return edges.filter( function( element, index, array ) {
return element.category.name === filterBy.category.name;
});
};
} );
Here is my html to get edges with categories filter
<select
name="category"
ng-model="filterBy.category"
ng-options="c.name for c in categories"
class="form-control"></select>
<ul>
<li ng-repeat-start="edge in edges | filter:{name: filterBy.search}| edges: filterBy " ng-click="selectEdge(edge)">
<span class="label label-default">{{ edge.category.name }}</span>
{{edge.name}}
<span class="text-muted">({{ displayRequirements(edge.requirements) }})</span>
</li>
<li ng-repeat-end ng-show="isSelected(edge)">
{{edge.description}}
</li>
</ul>
I formed My Plunker link is here.
Thanks
It doesn't work because of the category.name attribute. In your categoriesService.js you return collection where name equals to All. But if you look into EdgesService file, you'll see that there is no such option as 'All'. So this comparison in script.js file (in your filter)
return element.category.name === filterBy.category.name;
will always return false when filterby.category.name equals to 'All'.
The way to fix it is to change it to something like this:
return element.category.name === filterBy.category.name || filterBy.category.name === 'All';
This way it will always return true if 'All' category is selected.
Also later in the course rank option will be introduced as well. You can browse the code for that project here: https://github.com/Remchi/sw-front
Hope that helps. :)

Filtering a nested ng-repeat: Hide parents that don't have children

I want to make some kind of project list from a JSON file. The data structure (year, month, project) looks like this:
[{
"name": "2013",
"months": [{
"name": "May 2013",
"projects": [{
"name": "2013-05-09 Project A"
}, {
"name": "2013-05-14 Project B"
}, { ... }]
}, { ... }]
}, { ... }]
I'm displaying all data using a nested ng-repeat and make it searchable by a filter bound to the query from an input box.
<input type="search" ng-model="query" placeholder="Suchen..." />
<div class="year" ng-repeat="year in data | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.months | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.projects | filter:query | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
If I type "Project B" now, all the empty parent elements are still visible. How can I hide them? I tried some ng-show tricks, but the main problem seems so be, that I don't have access to any information about the parents filtered state.
Here is a fiddle to demonstrate my problem: http://jsfiddle.net/stekhn/y3ft0cwn/7/
You basically have to filter the months to only keep the ones having at least one filtered project, and you also have to filter the years to only keep those having at least one filtered month.
This can be easily achieved using the following code:
function MainCtrl($scope, $filter) {
$scope.query = '';
$scope.monthHasVisibleProject = function(month) {
return $filter('filter')(month.children, $scope.query).length > 0;
};
$scope.yearHasVisibleMonth = function(year) {
return $filter('filter')(year.children, $scope.monthHasVisibleProject).length > 0;
};
and in the view:
<div class="year" ng-repeat="year in data | filter:yearHasVisibleMonth | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | filter:monthHasVisibleProject | orderBy:sortMonth:true">
This is quite inefficient though, since to know if a year is accepted, you filter all its months, and for each month, you filter all its projects. So, unless the performance is good enough for your amount of data, you should probably apply the same principle but by persisting the accepted/rejected state of each object (project, then month, then year) every time the query is modified.
I think that the best way to go is to implement a custom function in order to update a custom Array with the filtered data whenever the query changes. Like this:
$scope.query = '';
$scope.filteredData= angular.copy($scope.data);
$scope.updateFilteredData = function(newVal){
var filtered = angular.copy($scope.data);
filtered = filtered.map(function(year){
year.children=year.children.map(function(month){
month.children = $filter('filter')(month.children,newVal);
return month;
});
return year;
});
$scope.filteredData = filtered.filter(function(year){
year.children= year.children.filter(function(month){
return month.children.length>0;
});
return year.children.length>0;
});
}
And then your view will look like this:
<input type="search" ng-model="query" ng-change="updateFilteredData(query)"
placeholder="Search..." />
<div class="year" ng-repeat="year in filteredData | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.children | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
Example
Why not a custom $filter for this?
Efficiency: the nature of the $diggest cycle would make it much less efficient. The only problem is that this solution won't be as easy to re-use as a custom $filter would. However, that custom $filter wouldn't be very reusable either, since its logic would be very dependent on this concrete data structure.
IE8 Support
If you need this to work on IE8 you will have to either use jQuery to replace the filter and map functions or to ensure that those functions are defined, like this:
(BTW: if you need IE8 support there is absolutely nothing wrong with using jQuery for these kind of things.)
filter:
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
'use strict';
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
map
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {
var T, A, k;
if (this == null) {
throw new TypeError(" this is null or not defined");
}
var O = Object(this);
var len = O.length >>> 0;
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
if (thisArg) {
T = thisArg;
}
A = new Array(len);
k = 0;
while(k < len) {
var kValue, mappedValue;
if (k in O) {
kValue = O[ k ];
mappedValue = callback.call(T, kValue, k, O);
A[ k ] = mappedValue;
}
k++;
}
return A;
};
}
Acknowledgement
I want to thank JB Nizet for his feedback.
For those who are interested: Yesterday I found another approach for solving this problem, which strikes me as rather inefficient. The functions gets called for every child again while typing the query. Not nearly as nice as Josep's solution.
function MainCtrl($scope) {
$scope.query = '';
$scope.searchString = function () {
return function (item) {
var string = JSON.stringify(item).toLowerCase();
var words = $scope.query.toLowerCase();
if (words) {
var filterBy = words.split(/\s+/);
if (!filterBy.length) {
return true;
}
} else {
return true;
}
return filterBy.every(function (word) {
var exists = string.indexOf(word);
if(exists !== -1){
return true;
}
});
};
};
};
And in the view:
<div class="year" ng-repeat="year in data | filter:searchString() | orderBy:'name':true">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.children | filter:searchString() | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in month.children | filter:searchString() | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>
Here is the fiddle: http://jsfiddle.net/stekhn/stv55sxg/1/
Doesn't this work? Using a filtered variable and checking the length of it..
<input type="search" ng-model="query" placeholder="Suchen..." />
<div class="year" ng-repeat="year in data | orderBy:'name':true" ng-show="filtered.length != 0">
<h1>{{year.name}}</h1>
<div class="month" ng-repeat="month in year.months | orderBy:sortMonth:true">
<h3>{{month.name}}</h3>
<div class="project" ng-repeat="project in filtered = (month.projects | filter:query) | orderBy:'name'">
<p>{{project.name}}</p>
</div>
</div>
</div>

AngularJS - ng-repeat show one item at a time

looking for some ideas here. i have a meal plan object that contains an array of meals. only one meal can be set as primary at a time but i want the user to be able to cycle through the array of meals and mark a meal as primary. i am stuck trying to figure out if ngrepeat makes sense here or ngswitch or ngshow. any thoughts or samples would be highly appreciated!
I have tried multiple approaches with no luck.
thanks
You could cycle through the meals by index of the meal and have a button to choose the meal like this:
http://jsfiddle.net/c6RZK/
var app = angular.module('mealsApp',[]);
app.controller('MealsCtrl',function($scope) {
$scope.meals = [
{name:'Meatloaf'},
{name:'Tacos'},
{name:'Spaghetti'}
];
$scope.meal_index = 0;
$scope.meal = {};
$scope.next = function() {
if ($scope.meal_index >= $scope.meals.length -1) {
$scope.meal_index = 0;
}
else {
$scope.meal_index ++;
}
};
$scope.choose = function(meal) {
$scope.meal = meal;
}
});
HTML
<div ng-app="mealsApp" ng-controller="MealsCtrl">
<div ng-repeat="m in meals">
<div ng-if="meal_index == $index">
<strong>{{m.name}}</strong>
<button ng-click="choose(m)">Choose</button>
</div>
</div>
<hr>
<button ng-click="next()">Next</button>
<hr>Your Choice: {{meal.name}}
</div>
You could just attach a property to the plan, with a flag that says whether or not it's the primary plan.
Here's a sample implementation:
$scope.plans = [{name:"One"}, {name:"Two"}, {name:"Three"}];
$scope.selectPlan = function(plan) {
for(var i = 0, l = $scope.plans.length; i < l; i++) {
$scope.plans[i].primary = false;
if($scope.plans[i] === plan) {
$scope.plans[i].primary = true;
}
}
};
HTML:
<ul>
<li ng-click="selectPlan(plan)" ng-repeat="plan in plans" ng-class="{primary: plan.primary}"><a href>{{plan.name}}</a></li>
</ul>
If you'd rather not attach properties you could use something like a selected index property on your controller.

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>

Resources