How to render only after a function has completed? - angularjs

I had a list of objects defined in my controller, and was sorting it after vm.init() like this:
const myItems = [{},{},{}...{}]
vm.init().then(function() {
$scope.ajaxLoading = true; // Loader
$scope.itemsToRender = [];
// Pushing items from myItems in this list according to some conditions
$scope.itemsToRender.sort(function(a, b) {
return a.order - b.order;
})
}).finally(function() {
$scope.ajaxLoading = false;
});
This was working fine as expected,
but now I am getting myList from api response in vm.init(), and it does not sort the list when I come to this page. But if I reload the page, it sorts the list and renders perfectly.
I tried debugging and put console.log in the function, and the function was being called.
What am I doing wrong? Is the list rendering before sorting is complete? How do I fix this?

AngularJS needs to be aware of changes in order to digest and reflect those in the template. The sort() method you are using is Javascript, so despite applying it directly to the $scope.itemsToRender variable Angular is still not aware its content has changed, probably because it uses properties such as length for arrays, so if the length does not change it won't recognize those changes.
Anyway I'm pretty sure you can use the orderBy filter instead which will handle all this for you and it's the best practise for this case
<tr ng-repeat="item in itemsToRender | orderBy:'id'">
https://docs.angularjs.org/api/ng/filter/orderBy

Related

AngularJS Filter for ng-repeat, how to get access to the filtered data

I have tried to make the title as descriptive as possible.
I have a set of data which I use ng-repeat to display in a table. There is also a filter applied so the user can type in a search bar and it will filter the results of the ng-repeat, there is nothing special about the filter (it is the default filter.
This obviously filters the data and shows it correctly.
The issue is, I also have a dropdown allowing the user to specify what sort of report they want.
The tablature data is raw, but they can select bar or line and it will show a graph. The data for the graph is created when they load the page. It takes the raw data and transforms it into labels and series.
What I would like to happen, is that when a filter is applied, the function transforms the filtered data, but I have no idea how I can actually get access to that data.
Does anyone know how I might go about it?
Update
I tried adding the filter to the controller and then doing my conversion in that function like this:
// Apply our filter
self.applyFilter = function () {
var filteredList = $filter('filter')(self.list, self.filter);
self.list = filteredList;
self.chartData = provider.getOverdueData(filteredList);
};
The problem with that, is that it is invoked many times and causes a $rootScope:infdig Infinite $digest Loop error.
Ok, so the error was caused by the new array which was being created each time applyFilter was invoked. I am not sure why an actual filter works (i.e. in the view) rather than in the function because they are doing the same thing ?!
But I was able to get around this issue by using angular.equals. First when I get the data, I had to store the result into a separate array, then when I invoke my filter I can use the unmodified array.
// Get our report
provider.overdueCollections().then(function (response) {
self.models = response;
self.list = response;
self.predicate = 'center';
self.chartData = provider.getOverdueData(response);
});
Once that was done, the table used self.list as it's datasource, but the applyFilter used the self.models to apply the filter to. It now looks like this:
// Apply our filter
self.applyFilter = function () {
// Create our filtered list
var filteredList = $filter('filter')(self.models, self.filter);
// Check to see if we are the same as the current list
var isSame = angular.equals(self.list, filteredList)
// If we are not the same
if (!isSame) {
// Update our scope
self.list = filteredList;
self.chartData = provider.getOverdueData(filteredList);
}
};
This meant that the data was only applied to the scope if it changed.

Angular CRUD, update view when backend/database changes ($resource and REST)

I am currently making an application in angular which does this:
(On page load) Make an api call in angular controller (to symfony2 end point) to get: items.
$scope.items = ItemsService.query(function(data){
$scope.loading = false;
}, function(err){
$scope.loading = false;
});
items is an array containing many item objects.
Each item contains parameters e.g. item.param1 item.param2.
I have built it in a similar way to this tutorial:
http://www.sitepoint.com/creating-crud-app-minutes-angulars-resource/
i.e. The angular controller calls a service which calls the (symfony2) backend api endpoint.
The endpoint passes back items which is gets from a database. Items are then put into the view using ng-repeat (item in items).
This all works fine.
Now, I have a button (in the ng-repeat) which effectively causes a PUT request to be made to (another symfony2 endpoint), thus updating item.param1in the database. This also happens in the tutorial I linked to.
The problem is that (in my application and in the tutorial) I have to again make an api call which updates ALL the items, in order to see the change.
I want to just update the data in the view (immediately) for one object without having to fetch them all again.
i.e. something like:
$scope.items[4] = Items.get({id: item.id}, function(){});
Except the application array key isn't known so I cant do that.
(so something like: $scope.items.findTheOriginalItem(item) = Items.get({id: item.id}, function(){});.
Another possible solution (which seems like it may be the best?). I have looked here:
http://teropa.info/blog/2014/01/26/the-three-watch-depths-of-angularjs.html
And tried doing the equality $watch: $scope.$watch(…, …, true);. Thus using watch to see when the item sub-array is updated. This doesn't seem to update the data in the view though (even though it is updating in the database).
Any advice on the best way of doing this (and how to do it) would be great! Thanks!
Essentially the button click should use ng-click to execute a function and pass the item to that function. Example:
...ng-repeat="item in items"...
<button type="button" ng-click="updateItem(item)">Update</button
...
Then in the function you have the exact item that you want to update. If you are using $resources, it would be something like:
$scope.updateItem = function(item) { item.$update(...); };
Unless I didn't understand you

Why does it improve ng-repeat performance?

Let's suppose a function called refreshList aiming to call a REST API to retrieve a JSON array containing items.
$scope.refreshList = function () {
// assuming getting the response object from REST API (Json) here
$scope.items = response.data.listItems;
}
And a basic ng-repeat on the HTML side:
<div ng-repeat="item in items track by item.id">
Then, I added a onPullToRefresh function, triggered by this directive, aiming to trigger a refresh of the whole list.
$scope.onPullToRefresh = function () {
$scope.items = [];
$scope.refreshList();
}
It's HTML is:
<ion-refresher on-refresh="onPullToRefresh()">
</ion-refresher>
The issue is that some pull-to-refresh (while scrolling to the top) can crash the app randomly...
Thus, I decided to alter the $scope.onPullToRefresh function like this:
$scope.onPullToRefresh = function () {
//$scope.items = []; commenting this line, causing the potential memory leak and the crash of the app
$scope.refreshList();
}
With this update, the app never crashes any more on my iOS device :)
However, I don't figure out what is the main difference by keeping the line emptying the array.
Does it impact directly the ng-repeat directive, causing some mess since the REST API's promise would reassign the content immediatly?
Indeed, Xcode IDE (since cordova app) indicated this error before applying this fix: EXC_BAD_ACCESS.
What might be the reason of this behavior?
I'm pointing out that Ionic (the framework I'm using) is fully tested with Angular 1.2.17 currently, so I'm using this version.
This probably isn't the greatest answer as I'm not 100% certain, but I would suggest it has something to do with the digest.
The ng-repeat likes to work with the one array and not be cleaned out between refreshes. It's designed more to allow you to dynamically add/remove items from the array and the view is updated accordingly in the digest loop. You can do some nice animations for items being added and taken away etc with this.
So when the digest arrives, you've scrapped the array it was watching and thrown a new array into the memory it had.
Try, instead of clearing the array $scope.items = [];, to .pop() each item in the array while it has length.
it will have very little impact to do it this way. Otherwise, the best "Angular" way for the ng-repeat is to do it by just adding the new items to the array and remove any that are now gone.

AngularJS: Deleting from scope without hardociding

I have an array of items bound to <li> elements in a <ul> with AngularJS. I want to be able to click "remove item" next to each of them and have the item removed.
This answer on StackOverflow allows us to do exactly that, but because the name of the array which the elements are being deleted from is hardcoded it is not usable across lists.
You can see an example here on JSfiddle set up, if you try clicking "remove" next to a Game, then the student is removed, not the game.
Passing this back from the button gives me access to the Angular $scope at that point, but I don't know how to cleanly remove that item from the parent array.
I could have the button defined with ng-click="remove('games',this)" and have the function look like this:
$scope.remove = function (arrayName, scope) {
scope.$parent[arrayName].splice(scope.$index,1);
}
(Like this JSFiddle) but naming the parent array while I'm inside it seems like a very good way to break functionality when I edit my code in a year.
Any ideas?
I did not get why you were trying to pass this .. You almost never need to deal with this in angular. ( And I think that is one of its strengths! ).
Here is a fiddle that solves the problem in a slightly different way.
http://jsfiddle.net/WJ226/5/
The controller is now simplified to
function VariousThingsCtrl($scope) {
$scope.students = students;
$scope.games = games;
$scope.remove = function (arrayName,$index) {
$scope[arrayName].splice($index,1);
}
}
Instead of passing the whole scope, why not just pass the $index ? Since you are already in the scope where the arrays are located, it should be pretty easy from then.

Linking MVC In AngularJS

I have a basic application in AngularJS. The model contains a number of items and associated tags of those items. What I'm trying to achieve is the ability to filter the items displayed so that only those with one or more active tags are displayed, however I'm not having a lot of luck with figuring out how to manipulate the model from the view.
The JS is available at http://jsfiddle.net/Qxbka/2 . This contains the state I have managed to reach so far, but I have two problems. First off, the directive attempts to call a method toggleTag() in the controller:
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggleTag(datum.id)'>{{datum.name}}</button>"
but the method is not called. Second, I'm not sure how to alter the output section's ng-repeat so that it only shows items with one or more active tags.
Any pointers on what I'm doing wrong and how to get this working would be much appreciated.
Update
I updated the method in the directive to pass the data items directly, i.e.
template: "<button class='btn' ng-repeat='datum in data' ng-click='toggle(data, datum.id)'>{{datum.name}}</button>"
and also created a toggle() method in the directive. By doing this I can manipulate data and it is reflected in the state HTML, however I would appreciate any feedback as to if this is the correct way to do this (it doesn't feel quite right to me).
Still stuck on how to re-evaluate the output when a tag's value is updated.
You can use a filter (docs) on the ng-repeat:
<li ng-repeat="item in items | filter:tagfilter">...</li>
The argument to the filter expression can be many things, including a function on the scope that will get called once for each element in the array. If it returns true, the element will show up, if it returns false, it won't.
One way you could do this is to set up a selectedTags array on your scope, which you populate by watching the tags array:
$scope.$watch('tags', function() {
$scope.selectedTags = $scope.tags.reduce(function(selected, tag) {
if (tag._active) selected.push(tag.name);
return selected;
}, []);
}, true);
The extra true in there at the end makes angular compare the elements by equality vs reference (which we want, because we need it to watch the _active attribute on each tag.
Next you can set up a filter function:
$scope.tagfilter = function(item) {
// If no tags are selected, show all the items.
if ($scope.selectedTags.length === 0) return true;
return intersects($scope.selectedTags, item.tags);
}
With a quick and dirty helper function intersects that returns the intersection of two arrays:
function intersects(a, b) {
var i = 0, len = a.length, inboth = [];
for (i; i < len; i++) {
if (b.indexOf(a[i]) !== -1) inboth.push(a[i]);
}
return inboth.length > 0;
}
I forked your fiddle here to show this in action.
One small issue with the way you've gone about this is items have an array of tag "names" and not ids. So this example just works with arrays of tag names (I had to edit some of the initial data to make it consistent).

Resources