Ionic/AngularJS Infinite scroll without call load more - angularjs

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.

Related

Is there a way to improve dynmically created html compile?

so i have a situation where in i'm creating html using jquery. I have no choice in this matter since I'm forced to use an old jquery plugin and integrate the created html in angular and the only way to do that is $compile. basically the flow is
var createbody = function(htmlcontents,scope){
$compile(htmlcontents)(scope);
}
the company internal jquery plugin is some sort of endless scroll for a table where it destroys and recreates a chunk of the tr's depending on the scroll position. so everytime you scroll, if the plugin needs to destroy and add tr's, createbody gets called.
the problem is that the scroll gets really laggy whenever it does the destroy and create part because of the compile. a directive is not an option at this point.
question: is there a way to cache the previously compiled chunk and use that later on whenever the plugin decides it needs to use that chunk again.? thanks
You can reuse compiled template:
var compiledTemplate = $compile(html);
compiledTemplate($scope1);
compiledTemplate($scope2);
compiledTemplate($scope3);
You can also see how ngRepeat reuses elements: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L469, but it's a bit more complicated.

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.

backbone render this.$el.html(this.template(data)) slow. what to do?

I have a collection to show.
1. for each model in collection, create a View
2. append view.render().el
I find view.render() takes long, more specifically
this.$el.html(this.template(data)) part.
I need to speed things up and found 'DOM manipulation' is slow.
So I looked for the ways to batch process the rendering but didn't find much.
Question 1.
I wonder if there is a way to batch process and attach the final html to the DOM without attaching each row to the DOM?
(I suspect this.$el.html() does the DOM manipulation. If so, can I somehow not perform the this.$el.html() call in view.render() and later assign the view's el to decrease the DOM interaction?)
Question 2.
Are there other pitfalls or performance blocker when redering views in clients?
edit
addAll: function() {
this.$('#thread-loop').html('');
var fragment = document.createDocumentFragment();
for (var i = 0; i < this.threads.length; i++) {
console.log('adding one');
var thread = this.threads.at(i);
var View = this.threadTypeToViewMap[thread.get('thread_type')];
var view = new View( {model: thread, forumSelector: this.forumSelector} );
fragment.appendChild(view.render().el);
}
this.$el.find('#thread-loop')[0].appendChild(fragment);
// this.threads.each(this.addOne, this);
},
Actually, I realised I'm using the fragment technique.
I nailed down the problem more and it looks like when javascript object has lengthy property (user created data), handlebar takes long to find the property (or so I suspect) in template() call.
Take a look at http://marionettejs.com
They extend Backbone to provide views that are optimized for collections (so you can create the DOM nodes in a loop and only add them to the document after you are done). This is going to really help you with performance.
As for question 2, the things that will give you pitfalls are:
Extensive DOM manipulation
Compiling templates on the client instead of the server (i.e. making individual requests for each template after loading your page)
General JavaScript performance bottlenecks (e.g. using polyfill foreach loops using jQuery or underscore instead of native JS, etc.)
Backbone template renders are slow because by default, they use JavaScript's with structure to scope the variable names properly as you expect. A little-known feature of Backbone templates allows you to skip this rather large performance penalty by assigning a variable to put all your data in (typically just "data"):
var t = _.template('<%= data.x %>', null, {variable: 'data'});
template({x: 42});
instead of the normal:
_.template('<%= x %>');
template({x: 42});
Do this, and you will see huge performance gains. This will probably solve your performance issues immediately.
Benchmark: http://jsperf.com/underscore-template-function-with-variable-setting
For Q1 ->
If your $el is not in the dom already, you can append all the $el's into a Document Fragment and then inject the Document Fragment into the page at once. JQuery's John Resig has written a nice writeup about how document fragments work : http://ejohn.org/blog/dom-documentfragments
A couple of things you could try on top of using the doc fragment:
1- If you want to use the for loop, cache the length prior to looping. var threadLength = this.threads.length;
2- That said, I would opt for using _.each instead of the for loop.
3- if you do 2, the threads model should be easily accessed. I don't know if there is any hit to using get(), but you could just access the model.attributes.thread_type directly.
3- Is your template cached?
4- Why are you changing the html of #thread-loop twice? Is that needed? If you need to access it twice, then cache that selector also.

Having a set of checkboxes map to a nested array

I am working on a SPA that pulls in customer data from one $resource call, and gets some generic preference data from another $resource call.
The preference data is sent as an array, which I want to use to populate a series of checkboxes, like so:
<div ng-repeat="pref in fieldMappings.mealPrefs">
<input type="checkbox"
id="pref_{{$index}}"
ng-model="customer.mealPrefs"
ng-true-value="{{pref.name}}" />
<label class="checkbox-label">{{pref.name}}</label>
</div>
When a user clicks one or more checkboxes, I want the values represented in that array of checkboxes to be mapped to an array nested inside a customer object, like so:
.controller( 'AppCtrl', function ( $scope, titleService, AccountDataService ) {
// this is actually loaded via $resource call in real app
$scope.customer = {
"name": "Bob",
"mealPrefs":["1", "3"]
};
// this is actually loaded via $resource call in real app
$scope.fieldMappings.mealPrefs = [
{'id':"1", 'name':"Meat"},
{'id':"2", 'name':"Veggies"},
{'id':"3", 'name':"Fruit"},
{'id':"4", 'name':"None"}
];
});
I have tried setting up ng-click events to kick off functions in the controller to manually handle the logic of filling the correct part of the customer object model, and $watches to do the same. While I have had some success there, I have around 2 dozen different checkbox groups that need to be handled somehow (the actual SPA is huge), and I would love to implement this functionality in a way that is very clean and repeatable, without duplicating lots of click handlers and setting up lots of $watches on temporary arrays of values. Anyone in the community already solved this in a way that they feel is pretty 'best practice'?
I apologize if this is a repeat - I've looked at about a dozen or more SO answers around angular checkboxes, and have not found one that is pulling values from one object model, and stuffing them in another. Any help would be appreciated.
On a side-note, I'm very new to plunkr (http://plnkr.co/edit/xDjkY3i0pI010Em0Fi1L?p=preview) - I tried setting up an example to make it easier for folks answer my question, but can't get that working. If anyone wants to weigh in on that, I'll set up a second question and I'll accept that answer as well! :)
Here is a JSFiddle I put together that shows what you want to do. http://jsfiddle.net/zargyle/t7kr8/
It uses a directive, and a copy of the object to display if changes were made.
I would use a directive for the checkbox. You can set the customer.mealPrefs from the directive. In the checkbox directive's link function, bind to the "change" event and call a function that iterates over the customer's mealPrefs array and either adds or removes the id of the checkbox that is being changed.
I took your code and wrote this example: http://plnkr.co/edit/nV4fQq?p=preview

Resources