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.
Related
While transitioning an existing angular site, I encountered an annoying problem. The initial symptom was that a certain controller was not running it's initialize function immediately following the login. I logged and I tracked, and eventually I realized it was a design flaw of the page. Essentially, index.html contains a <header>, <ng-view>, and <footer>. There are a couple of ng-if attributes that live in the header that I want to evaluate after the login, but since the view is the only thing that is reloaded, it was not reinitializing the header controller, and thus not updating the ng-if values.
Then I was reminded of ngInclude, which seems like the perfect solution, until I got it hooked up and realize that doesn't work either. It loads the template the first time, and doesn't reinitialize when the view changes. So then I got the bright idea of passing the HeaderController to another controller or service, and controlling this one stubborn boolean value through a proxy of sorts. That also didn't work. Then I tried putting a function and a boolean into another service, and mirroring that property in the header controller, but thus far I have not gotten this working.
I have done plenty of research about multiple views in the index, and so far I hear a lot about this ui-router, but I'm still not convinced that is the way I want to go. It does not seem to be a simple solution. I have not tried putting the ng-include into the templates yet either, because then I feel like that is going back in time to when we had to update 100 pages every time we changed the menu.
I lost a whole day to this. If anyone could tell me how to trigger the evaluation of this one property in my header controller which I would like to live outside the other templates, please let me know!
Ok so you need to know in your HeaderController when the view has reloaded. There's a number of ways of doing this but the easier and maybe the more correct in this particular case is with an event.
So when you are refreshing the view you just do this, let's say you need the new value of ob1 and ob2 variables.
// ViewController
$rootScope.$emit('viewRefresh', {ob1: 'newvalue1', ob2: 'newvalue2'});
And in your HeaderController you need to listen for that event, and set on your $scope the new values for those attrs (if you're not using controller as syntax).
// HeaderController
$rootScope.$on('viewRefresh', function onRefresh(event, data) {
$scope.ob1 = data.ob1;
$scope.ob2 = data.ob2;
})
Another Solution
Sharing a Promise through a Service (using $q)
function HeaderService($q) {
var defer = $q.defer();
return {
getPromise: function() {return defer.promise},
notify: function(data) {defer.notify(data)}
}
}
function HeaderController(HeaderService) {
var vm = this;
HeaderService.getPromise().then(function(data) {
vm.ob1 = data.ob1;
vm.ob2 = data.ob2;
})
}
function ViewController(HeaderService) {
var data = {ob1: 'newvalue1', ob2: 'newvalue2'};
HeaderService.notify(data)
}
I'd like to implement an infinite scroll, but without call a load more function everytime.
Let's think I've download an array of 1000 items from Parse. I'd like to show 10 items and then add more and more items to my list using ng-repeat from a local array.
I think the best solution is to implement a directive, but maybe there is something already done...
Anyone?
Sure you could do it as a directive, but regardless, you are going to need some sort of loadMore function. To put in other words, you are going to need to detect when the user has scrolled to a particular position, and perform some updating function.
While your own directive might be better at encapsulating your specific requirement, you could achieve what you want with any other current infinite scrolling plugin. Simply keep two versions of your array in your model. One is the data, while the other is the data that the user sees. If your arrays are of objects, then both should be able to work on the same items.
For example:
var data = [...];
var position = 0;
var pageSize = 10;
$scope.viewable = [];
$scope.loadMore = function(){
$scope.viewable = $scope.viewable.concat(data.slice(position, position + pageSize));
position += pageSize;
};
Update:
If this is something you need to implement often, but don't want to implement your own scrolling related directive, you could encapsulate the idea above in a factory that manages multiversion arrays. You would still have to hook the loadMore, and setup the multiversion, but your controller and view code would look like the following:
// the data you loaded somewhere
var data = [...];
$scope.specialArray = PageableArrayFactory.create(data);
// then in your html
ng-repeat="item in specialArray.viewItems"
// where you put your infinite scroll
infinite-scroll="specialArray.loadMore()"
PageableArrayFactory just needs to be a factory that takes in your big data array, and keeps track of a viewable copy array like my initial example. It shouldn't be too hard to implement, and can then be reused with a single line of code in any controller after you load your data.
Another example:
You could also build a custom filter on $index (or use ng-show/if), so that ng-repeat only shows the items that you want. You will still need to hook loadMore() though if you want to use existing infinite scroller code, which means you will still need something my factory example.
Matt's solution is perfectly fine. Especially considering you've mentioned you want to use ng-repeat and you want to paginate your result.
It doesn't seem to be a huge effort to implement what he's suggesting. The other option is to use collection-repeat, especially for performances.
The implementation is very simple:
<ion-content>
<ion-item collection-repeat="item in items">
{{item}}
</ion-item>
</ion-content>
and you don't have to be worried about loading all the items together as the framework will be responsible to load chunks of information for you.
The only drawback - as far as I am aware - is you cannot control the number of items you want to display.
When I update my $scope like so in a controller
$scope.item = "Hello";
Then the whole DOM for item seems to be removed and then added again. This seems fine, but if I have a list of items and do
$scope.items = Resource.query();
To update all the items then all of the DOM for items is removed and then re-added, this looks a broken and clumsy - is there anyway around this removing and then adding of the DOM elements when $scope is updated?
This issue is further exasperated if $scope.items and its children are used inside several ng-repeat statements as all of those ng-repeat sections are removed and then re-added.
EDIT
I have read this and feel that this is the issue https://www.binpress.com/tutorial/speeding-up-angular-js-with-simple-optimizations/135
That I have so much "stuff" going on the $digest is just slow. I am working on an example but in the mean time imagine this try of data
{
{
id: 1,
name: "name1",
something: {
id: 10,
name: "something10"
else: {
id: 15,
name: "else15"
}
}
}
}
But there are 20 such objects all with nested objects - this appears to be the issue. That there are so many objects being parsed and bound to the DOM that the $watchers are just taking a long time to go over everything.
EDIT 2
I made this demo, perhaps I am using resource wrong? http://plnkr.co/edit/QOickL0Dyi8jmuvG9mzN
But the items are replaced every 5 seconds, on replace they all disappear and then reappear. This is the issue I am having.
This is a common problem when using ng-repeat. In order to track the objects the directive itself by default adds additional property to every object in the array ($$hashkey). And whenever new object is added or removed from the array the same is happening for it's associating DOM element. When the array is replaced with new array (ex. returned from the server) as you mentioned all the DOM elements representing objects from the previous array are removed and replaced with new DOM elements for the new objects. This is simply because the new objects in the array does not have the $$hashkey property even though they may be semantically the same.
A solution for this common problem came in Angular 1.2 version with the track by clause. You can find more detailed explanation about it in this post.
In your case the objects in the array have an 'id' property which it's a good candidate for tracking them if you can guarantee it's uniqueness. So using ng-repeat like this will improve it's performance.
<div ng-repeat="item in items track by item.id">
<!-- tpl -->
</div>
If you'll take a look at $resource documentation, you'll see that the correct way of manipulation with data - to use callback function
Instead of
$scope.items = Resource.query();
Try
Resource.query(function(data) {
$scope.items = data;
});
Did you try ngCloak directive? https://docs.angularjs.org/api/ng/directive/ngCloak
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
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.