AngularJS: Trigger function on filter change - javascript

I'm paginating an Angular table and want to display all the page numbers beneath the table.
I'm planning to create an array of the page numbers and then use ng-repeat to display them all:
HTML
<tr ng-repeat-start="item in c.filteredList = (c.data | dynamicFilter:c.filter | orderBy:c.sortOrder.order:c.sortOrder.reverse)">
JS
this.checkPage = function(){
this.pageNumArr = [];
for(i=0; i<this.filteredList.length/this.perPage; i++){
this.pageNumArr.push(i);
}
}
Where this.perPage is the number of items per page (set by the user).
What I can't figure out is how to trigger checkPage() whenever the filter changes.

You would be best binding your page number ng-repeat to a function that creates and returns the array of page numbers. This will create a watcher for the function and keep the array of page numbers up to date.
There will be no need to manually create a $watch in your controller.
this.pageNumbers= function(){
var pageNumArr = [];
for(i=0; i<this.filteredList.length/this.perPage; i++){
pageNumArr.push(i);
}
return pageNumArr
}
<span ng-repeat="page in c.pageNumbers()">{{page}}</span>

I think that triggering events inside a filters shouldn't be considered a best practice, probably, you need to find another approach.
By the way, there are many way:
If you can edit that filter, simply, pass the $scope reference to it and trigger the event via $scope.emit or $scope.broadcast: <li ng-repeat="item in items | myFilter:[param1, param2, paramN]"></li>
Angular supports filter inside a controller, so, probably this should be a better solution https://toddmotto.com/everything-about-custom-filters-in-angular-js/ (have a look at Filter 4: Controller/$scope filter);
Register a watcher on your model, but, this is bad for performances...

You can watch for the filteredList and call the checkPage() there:
var self = this;
$scope.$watch(
function() {
return self.filteredList.length;
},
function(newValue, oldValue) {
if (newValue !== oldValue) {
self.checkPage();
}
}
);

Related

How data binding is working in filters in AngularJS?

This is my button
<button ng-repeat="item in data" ng-click="selectCategory(item)">{{item}</button>
And this is my controller
.controller("productListCtrl", function ($scope) {
let selectedCategory = null;
$scope.selectCategory = function (item) {
selectedCategory = item;
};
$scope.categoryFilterFn = function (product) {
return selectedCategory == null || product.category == selectedCategory;
}
});
I have created method categoryFilterFn for filter filter and I use it in other part of my code
<div ng-repeat="item in data| filter:categoryFilterFn" class="well">
so I use filter for this div. When the page is loaded first time filter is working, but when I click button it goes to selectCategory() function and changes the value of selectedCategory variable(which is not even a $scope property, just a variable). After that angular filters the ng-repeat again. I want to understand why ? I understand that my variable selectedCategory is used in categoryFilterFn function (which is filter), but does it mean that every change of it will cause new rendering ?
In angularjs in every change detection filter gets executed. When you are invoking $scope.selectCategory function angular internally run the digest cycle and firing events to identify the changes happening in the scope. So each digest cycle or change detection filters are gonna fire again.

Angular Directive Template Doesn't Bind To Angular Controller Variable

I have a curious case that I can't figure out...
I have a directive on my app like so:
app.directive('cartTotal', function() {
return {
template: "<i ui-sref='cart' class='fa fa-shopping-basket'></i><span class='items'>#{{cartQTotal.total}}</span>"
};
});
When I load the page, this function fires:
if(localStorage.getItem("cart") != null)
{
console.log("makeacart");
var cart = JSON.parse(localStorage.getItem("cart"));
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
$('.fixed-cart').animateCss('bounce');
}
This works.
But if I modify $scope.cartQTotal outside of this, such as in a function (still in the parent controller but derived from an ng-click()) for example:
$scope.add2Cart = function(name){
var cart = JSON.parse(localStorage.getItem("cart"));
for(var zz = 0;zz<cart.length;zz++)
{
if(cart[zz].item == name)
{
console.log("already in cart");
$('.fixed-cart').animateCss('bounce');
return;
}
}
cart.push({item:name,cartQ:1});
localStorage.setItem("cart", JSON.stringify(cart));
console.log("makeacartii");
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
console.log($scope.cartQTotal.total);//THE NUMBER I WANT
$('.fixed-cart').animateCss('bounce');
}
On //The Number I Want line I get the proper number, as in the variable is correct but my directive template doesn't update. I don't understand why not.
Please assist.
Edit (from the docs):
Observing directives, such as double-curly expressions {{expression}},
register listeners using the $watch() method. This type of directive
needs to be notified whenever the expression changes so that it can
update the view.
So I guess the question is how do I notify the directive properly?
EDIT 2:
Looking at it using the nginspector extension, it appears I have two scopes with cartQTotal rather than one, this remains constant whether or not I have the directive.
I am very confused because I have my controller scope and then a duplicate scope with all the same variables but the cartQTotal changes in one scope and not the other. Why would I have a duplicate but unnamed controller scope?
This is because your directive and $scope and the controller where data is updating both are different..
So you need to pass your controller data to your directive so that it will get modified. For this purpose you can use $broadcast (but make sure you know about it because in large application its not good practice to use it).
So Try this
Controller
cart.push({item:name,cartQ:1});
localStorage.setItem("cart", JSON.stringify(cart));
console.log("makeacartii");
$scope.cartQTotal.total = 0;
for(i=0;i<cart.length;i++)
{
$scope.cartQTotal.total += cart[i].cartQ;
}
console.log($scope.cartQTotal.total);//THE NUMBER I WANT
$('.fixed-cart').animateCss('bounce');
$rootScope.$broadcast("cartUpdated",$scope.cartQTotal);
directive
$scope.$on('eventEmitedName', function(event, data) {
$scope.cartQTotal = data;
});
It was a problem as elucidated here: How do I share $scope data between states in angularjs ui-router?
Basically I didn't realize that my ui-router configuration was creating a seperate instance of my controller. Changing my code as specified in that answer allowed it to work properly, even though I wasn't changing states it still affected the directive's ability to communicate with the proper controller instance.
Data you wish to use within your directive that is manipulated outside of the directive should be passed in using bindings. There's a great short read here that shows you how. Personally, I use method 6 the most.
The gist is - you add to your directive's returned object:
scope: {
yourVariable: '=', //use =? for optional variables
}
And then use it in your directive as such:
<span>{{your-variable}}</span>
And bind to it as such:
<my-directive your-variable="myControllerVariable"></my-directive>

angularjs: Bound data in custom direct no longer updating after using "track by" in ng-repeat

This is an addendum to this question I asked previously:
Why does my custom directive not update when I derive the output from the bounded data?
My current dilemma is that I have duplicates in data that I generate inside a custom directive used in an ng-repeat. This means I have to use "track by". This somehow breaks the binding and when I update the model, it no longer updates. If I don't use update and remove the duplicates (which for the example can be done easily but for my real problem I cannot), it works. Here is the jsfiddle of how the issue:
http://jsfiddle.net/Lwsq09d0/2/
My custom directive has this:
scope: {
data: "="
},
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.data
}, function () {
var getPages = function(extra) {
var pages = [];
pages.push('...');
for (var i = scope.data.list[0]; i <= scope.data.list[1] + extra; i++) {
pages.push(i);
}
pages.push('...');
return pages;
}
scope.pages = getPages(1);
}, true);
},
// Remove "track by $index" to see this working and make sure to remove the duplicates
// "..." pushed in to the generated data.
template: '<ul><li ng-repeat="d in pages track by $index" my-button="d"></li></ul>'
In the fiddle, I have an ng-click call a controller function to modify data.
I've seen other questions about track by breaking binding, but I haven't seen one where the ng-repeat variable is generated in the custom directive via the bound data.
Thanks for any help.
Track by is optimized not to rebuild the DOM for already created items. In your example, you are using $index as the identifier. As such, ng-repeatsees the identifier 1 (for the second element in the pages array) and decides that it does not have to rebuild the DOM. This is causing the problem that you are experiencing.
One possible solution might be to generate page Objects that have a unique id, and to track by that:
var lastID = 0;
function createPage(name){
return { name: name, id: lastID++ };
}
// ... Directive code
pages.push(createPage('...')); // Do this for everything that you push to pages array
// ... More directive code
template: '<ul><li ng-repeat="d in pages track by d.id" my-button="d.name"></li></ul>'
Your JSFiddle, updated to work: http://jsfiddle.net/uv11fe93/

Saving with angular $resource.save causes view to redraw/reflow due to collection watcher

I have a model which I load and store using $resource. The model is an aggregate and has nested collections inside, which are binded to an html view using ng-repeat.
Model:
{
someRootField: "blabla",
sectionCollection: [
{
name: "section1"
....
},
{
name: "section2",
....
}
]
}
html:
<div ng-repeat="section in myModel.sectionCollection">
...
</div>
controller:
MyModelResource = $resource(config.api4resource + 'models/:id', {id:'#_id'});
$scope.myModel = MyModelResource.get({id: xxxx});
The problem: when I use $save on this model, it causes a reload/redraw of some portions of the screen (seems not the root fields, but the collection related ones), if some binded elements within the sections are inputs, focus is lost too. I did some debugging and here is what I think is happening.
When I save the model, the results from the POST command mirror the body of the request, and myModel is being repopulated with it. Simple fields in the root of the model are pretty much the same, so the watch() mechanism doesn't detect a change there, however the the objects in the sectionCollection array are different, as they are compared not by their contents but by an equality of the references and fail, this causes the ui controls associated with the collection to be completely reloaded/redrawn.
There is this code in $watchCollectionWatch() in angular:
} else if (isArrayLike(newValue)) {
if (oldValue !== internalArray) {
// we are transitioning from something which was not an array into array.
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
newLength = newValue.length;
if (oldLength !== newLength) {
// if lengths do not match we need to trigger change notification
changeDetected++;
oldValue.length = oldLength = newLength;
}
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
}
in my case, I've definitely seen the oldValue[i] = newValue[i] comparison fail, the objects were different. One of the reason is oldValue contained variables prefixed with $ that were referring back to the scopes that were previously created for each item.
The question is, how can I prevent a reflow? Or how can I do it differently to avoid it. Keeping myself two copies of the model, one for $resource and another for binding to view and synchronizing between them manually does not seem right.
Thanks!
You can use $http service to avoid model updates that $save cause:
$scope.save2 = -> $http.get 'blah_new.json'
I used get in example but you can use whatever you need from this list of shortcut methods. And here is a simple example plunk.
Also it's simple to save elemen's focus after rerendering:
$scope.save = ->
active = document.activeElement.getAttribute 'id'
$scope.user1.$save ->
document.getElementById(active).focus()

Knockout binding on foreach item not updating

I am using the click event on a button to set the value of an item that was generated using a foreach.
<table>
<tbody data-bind="foreach: Employees">
<a data-bind="click:$parent.delete()">
..
in my delete function I am setting the value but it doesn't update the screen
Delete :(emp) {
emp.active=false;
}
When I create I am setting all the individual properties as observable but seems like they are not when in the foreach loop.
Update
Employees is filtered.computed
var Employees=ko.computed(function() {
return ko.utils.arrayFilter(AllEmployees(), function (empid) {
return empid.ID == filter();
});
When you get/set observables you need to call them like this:
var val = obj.prop(); //Getter
obj.prop(false); //Setter
One other issue you have is that you are using parenthesis in your click binding. Remember that Knockout bindings are just javascript, so it will actually execute that expression when it binds.
You need to get rid of those parenthesis or emp will be undefined initially.
UPDATE:
I've updated this jsFiddle to include three filtered lists similar to what you have shown above. You can see that using a filtered list via a computed has no bearing on how knockout handles the bindings, and the UI updates seamlessly.
http://jsfiddle.net/jwcarroll/ceRPK/
To set an observable, you have to call it (since observables are implemented as functions):
emp.active(false);
Your method simply overwrites the observable.
Knockout subscribes to the observable array, but not to each observable within that array. If you want to subscribe to individual properties you need to subscribe manually by using myObservable.subscribe()
Knockout subscribes to the observable array, but not to each observable within that array. If you want to subscribe to individual properties you need to subscribe manually using myObservable.subscribe()
Edit
If you are trying to have your computed keep track of what should be in your computed you can do so like this -
var allEmployees = ko.observableArray([my data goes here]);
var Employees=ko.computed(function() {
return ko.utils.arrayFilter(allEmployees(), function (emp) {
return emp.active === true;
});
});
That works if active is not an observable property of each allEmployees(). If it is an observable just change that to -
var allEmployees = ko.observableArray([my data goes here]);
var Employees=ko.computed(function() {
return ko.utils.arrayFilter(allEmployees(), function (emp) {
return emp.active();
});
});

Categories

Resources