lightweight infinite scroll with backbone.js - backbone.js

i've looked at pagination in backbone https://gist.github.com/838460, and it all seems very heavy handed for what I'm looking for.
I want to do an infinite scroll type paging, and I'm new to backbone, so maybe I'm just not understaning it correctly.
what I thought I would do is get the first collection, click a 'next' button, and get the results and just append that to the original collection and render the newly added items.
So I have this in my Router I have an index function
if(!myApp.list){
myApp.list = new myApp.collections.list;
myApp.list.page = 1;
} else {
myApp.list.page++;
}
myApp.list.url='/recipes?page='+myApp.list.page;
myApp.list.fetch({
add: true,
success: function() {
new myApp.views.list({ collection: myApp.list});
},
error: function() {
new Error({ message: "Error loading documents." });
}
});
which will create the collection if it does't exist, and if it does exist, increment the 'page' before requesting the next items in the list.
so the first part of my question is, is there anything wrong with this way of doing things?? Seems much simpler than the other solutions I've seen.
Question #2 seems ridiculous, but how do I then trigger the 'next' button to get the next list??
In my view, I have a 'next' button, but calling myApp.routers.list.index or myApp.views.list doesn't give me an updated list.

It's normal that myApp.routers.list.index() doesn't work if there is the declaration of the Router, you need to call the instance of the router.
There are many things to say and I think the best explication is to see the code work and if It's that you want, learn the code.
I created an infinite listing with a "More" button to add models on the listing with using your code. The demo is on nodejitsu here : http://infinite-scroll.eu01.aws.af.cm/
You can show the complete code (client and server) on my gist on GitHub : https://gist.github.com/1522344 (I added a comment to explain how use the files)

Here's a lightweight implementation https://github.com/joneath/infiniScroll.js

Here is another solution http://backbonetutorials.com/infinite-scrolling/

I have created an extend of Backbone.Collection for easy using:
_.extend Backbone.Collection.prototype,
options:
infinitescroll:
success: $.noop
error: $.noop
bufferPx: 40
scrollPx: 150
page:
current: 0
per: null
state:
isDuringAjax: false
isDone: false
isInvalid: false
loading:
wrapper: 'backbone-infinitescroll-wrapper'
loadingId: 'backbone-infinitescroll-loading'
loadingImg: 'loading.gif'
loadingMsg: '<em>Loading ...</em>'
finishedMsg: '<em>No more</em>'
msg: null
speed: 'fast'
infinitescroll: (options={})->
# NOTE: coffeescript cannot deal with nested scope!
that = #
_.extend(#options.infinitescroll, options.infinitescroll) if options.infinitescroll
infinitescroll_options = #options.infinitescroll
# where we want to place the load message in?
infinitescroll_options.loading.wrapper = $(infinitescroll_options.loading.wrapper)
if !infinitescroll_options.loading.msg and infinitescroll_options.loading.wrapper.size() > 0
infinitescroll_options.loading.msg = $('<div/>', {
id: infinitescroll_options.loading.loadingId
}).html('<img alt="'+$(infinitescroll_options.loading.loadingMsg).text()+'" src="' + infinitescroll_options.loading.loadingImg + '" /><div>' + infinitescroll_options.loading.loadingMsg + '</div>')
infinitescroll_options.loading.msg.appendTo(infinitescroll_options.loading.wrapper).hide()
else
infinitescroll_options.loading.msg = null
fetch_options = _.omit(options, 'infinitescroll')
infinitescroll_fetch = ()=>
# mark the XHR request
infinitescroll_options.state.isDuringAjax = true
# increase page count
infinitescroll_options.page.current++
payload = {
page: infinitescroll_options.page.current
}
payload['limit'] = infinitescroll_options.page.per if infinitescroll_options.page.per isnt null
_.extend(fetch_options, {
remove: false
data: payload
})
if infinitescroll_options.loading.msg
# preload loading.loadingImg
(new Image()).src = infinitescroll_options.loading.loadingImg if infinitescroll_options.loading.loadingImg
infinitescroll_options.loading.msg.fadeIn infinitescroll_options.loading.speed, ()->
that.fetch(fetch_options)
.success (data, state, jqXHR)=>
infinitescroll_options.state.isDuringAjax = false
infinitescroll_options.state.isDone = true if _.size(data) is 0
infinitescroll_options.loading.msg.fadeOut infinitescroll_options.loading.speed, ()->
infinitescroll_options.success.call(data, state, jqXHR) if _.isFunction(infinitescroll_options.success)
return
return
.error (data, state, jqXHR)=>
infinitescroll_options.state.isDuringAjax = false
infinitescroll_options.state.isInvalid = true
infinitescroll_options.loading.msg.fadeOut infinitescroll_options.loading.speed, ()->
infinitescroll_options.error.call(data, state, jqXHR) if _.isFunction(infinitescroll_options.error)
return
return
return
else
that.fetch(fetch_options)
.success (data, state, jqXHR)=>
infinitescroll_options.state.isDuringAjax = false
infinitescroll_options.state.isDone = true if _.size(data) is 0
infinitescroll_options.success.call(data, state, jqXHR) if _.isFunction(infinitescroll_options.success)
return
.error (data, state, jqXHR)=>
infinitescroll_options.state.isDuringAjax = false
infinitescroll_options.state.isInvalid = true
infinitescroll_options.error.call(data, state, jqXHR) if _.isFunction(infinitescroll_options.error)
return
return
$(document).scroll ()->
$doc = $(document)
isNearBottom = ()->
bottomPx = 0 + $doc.height() - $doc.scrollTop() - $(window).height()
# if distance remaining in the scroll (including buffer) is less than expected?
(bottomPx - infinitescroll_options.bufferPx) < infinitescroll_options.scrollPx
return if infinitescroll_options.state.isDuringAjax || infinitescroll_options.state.isDone || infinitescroll_options.state.isInvalid || !isNearBottom()
infinitescroll_fetch()
return
infinitescroll_fetch()
return
You can see the implementation at https://gist.github.com/mcspring/7655861

jScroller plug-in https://github.com/renatosaito/jscroller

Here is another solution https://gist.github.com/davidmontoyago/6336612#file-infinite-list-view-js with jquery waypoints.

Check out the Backbone Infinite Scroll
It's based on a single model and a single view.
Here is a demo site

Related

Trying to bind in ng-init a value. This value was set afler model loading, and 2 ajax calls

I explain myself,
The application i'm making right now come from a derivative of phonegap. I must absolutely waiting ajax answer to set de ng-init. I know i can do something like this ng-init = "ng-model = arr[0]" or i can use a variable for le position of the array like ng-init = "ng-model = arr[position]". That is simple in most case, but me because i'm working on a derivative phonegap i must call my function from a focus method with an ajax call, plus waiting after the result of another ajax call. See what i mean:
HTML select code:
<select id="usSelect" class="geotabFormEditField" ng-init="usCycle = usCyclesArr[usPos]" ng-model="usCycle" ng-options="x.descEn for x in usCyclesArr" ng-change="usSel()"}></select>
Function where i call my angular function to define which option must be selected. I must absolutely call it in this function.
focus: function (freshApi, freshState) {
freshApi.getSession(session => {
database = session.database;
freshApi.call('Get', {
typeName: 'User',
search: {
name: session.userName
}
}, function(user){
lang = user[0].language;
glScope.getHosRules(database);
}, function(){
//Oops can't get user
});
});
},
function called is getHosRule.
The angular array:
$scope.usCyclesArr = [{cycleId: 'America8Day', descEn: 'USA Property 70-hour/8-day', descFr: 'Propriété É.-U. 70 heures/8 jours'}, {cycleId: 'America7Day', descEn: 'USA Property 60-hour/7-day', descFr: 'Propriété É.-U. 60 heures/7 jours'}, {cycleId: 'America7DayBig', descEn: 'USA Property 60-hour/7-day (16-hour exemption)', descFr: 'Propriété É.-U. 60 heures/7 jours (exemption de 16 heures)'}, {cycleId: 'America7DayNo34h', descEn: 'USA Property 60-hour/7-day without 34-hour reset', descFr: ''}, ...];
function to get which option must be selected:
$scope.getHosRules = function(dat) {
console.log(dat, url);
$http({
method: 'GET',
url: $scope.urlPath + 'borderCross/' + dat + '/getHosRules',
responseType: 'json',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
}).then(function successCall(response) {
//console.log(response.data);
for(var i = 0; i < $scope.usCyclesArr.length; i++) {
//console.log($scope.usCyclesArr[i].cycleId);
if($scope.usCyclesArr[i].cycleId === response.data[0].us_id) {
$scope.usPos = i;
}
}
}, function errorCall() {
console.log('Unexpected error');
});
}
The problem is usPos get is value after ng-model is loaded and i must waiting for ajax answer, so it desn't working. I tried ng-init="{{usCycle = usCyclesArr[usPos]}}" It seem working but an error appear in the console and i can't take the risk on a professional app. So Anybody knows how can i bind usPos variable ?
From what I can understand from your question, you need to set the value of $scope.usCycle which will be shown as the selected value of dropdown.
Rather than setting the value in ng-init, why are you not setting in for loop:
for(var i = 0; i < $scope.usCyclesArr.length; i++) {
//console.log($scope.usCyclesArr[i].cycleId);
if($scope.usCyclesArr[i].cycleId === response.data[0].us_id) {
$scope.usCycle = usCyclesArr[i]; // <-- directly assigning value
}
}
Few points I would like to highlight:
You are facing issues because your ng-init works with AngularJS lifecycle where as your Business logic is triggered from focus function which is not at all connected with AngularJS. So, its a bad choice to mix code like this. To provide better UX, you can use spinner to make the user wait unless the $scope.usCycle value is assigned.
Rather than iterating through entire loop of for(var i = 0; i < $scope.usCyclesArr.length; i++) , you can break break the loop once you find the value. It will save extra looping cycles.
I found the way myself and it's simple. Refreshing my ng-model with the simple code below and removing ng-init attribute in html file.
$scope.usCycle = $scope.usCyclesArr[$scope.usPos];
Dropdown take right position.
Of course like Vivek said, breaking the loop when position is found is better.

backbone fetch on a nested route

I have a Sitesand a Positionscollection. Each time the user selects a new site, the id is sent to the refreshPositions method which is in charge of doing the fetch call.
The route to get the positions look like this '.../sites/1/positions'
view.js
refreshPositions: function(siteId) {
this._positions.fetch({
success: this.onPositionsFetchSuccess.bind(this),
error: this.onPositionsFetchError.bind(this)
});
},
So refreshPositions is called whenever I need to update the positionson the page and the siteId parameter has the id, I just don't know to tell fetch to route to something like .../sites/n/positions where n would be the siteId .
Sorry if I missed relevant informations for my question, I'm pretty new to backbone.
I see, so you are calling fetch from your Positions Collection. The out-of-the-box functionality there is to fetch the whole collection (every Position object) if you have a RESTfull api set up. If you want more specific behaviour from your collection, you can probably write it into the Collection object definition.
var PositionCollection = Backbone.Collection.extend({
initialize: function(models, options) {
this.siteId = (options && options.siteId) || 0;
},
url: function() {
if (!this.siteId) {
return '/positions'; // or whatever
}
return '/sites/' + this.siteId + '/positions';
},
// etc...
});
Then, assuming that _positions refers to an instance of PositionCollection you can do:
refreshPositions: function(siteId) {
this._positions.siteId = siteId; // or wrap in a setter if you prefer
this._positions.fetch({
success: this.onPositionsFetchSuccess.bind(this),
error: this.onPositionsFetchError.bind(this)
});
},

Jaydata saveChanges() counts tracked / changed entities, but doesn't send a batch request (with OData v4 Provider and Web Api)

by working with jaydata i am adding entities to a tree structure with deep nesting of entity-objects.
I attach the upper entity to the context and edit/add/remove related child entities. At the end i use saveChanges() with a promise.
The count-value passed to the promise tells that all changed entities have been counted correctly but saveChanges() didn't execute a batch request, persisting these entities.
So it feels like nothing else happens, but counting entities.
I post a small code example. I am quite sure, that the references of the entites are set correctly. (Working with jaydata, odata v4, web api and angularjs)
Is someone else having this problem with jaydata and found the reason?
Thanks for your help. =)
Greetings Paul
// Attach upper entity
DataService.jaydata.attach(viewModel.currentSkillTree.entity);
// Generating new entities
var newSkill = new DataService.jaydata.Skills.elementType({
Id: undefined,
Name: 'New skill',
Levels: [],
IconId: 47,
SkillTreeUsage: []
});
var newSkillLevel = new DataService.jaydata.SkillLevels.elementType({
Id: undefined,
ShortTitle: 'New level',
Skill: newSkill,
SkillId: undefined,
Level: 1,
RequirementSets: []
});
var newRequirementSet = new DataService.jaydata.RequirementSets.elementType({
Id: undefined,
SkillLevel: newSkillLevel,
SkillLevelId: undefined,
SkillTree: undefined,
SkillTreeId: viewModel.currentSkillTree.entity.Id,
});
var newSkillTreeElement = new DataService.jaydata.SkillTreeElements.elementType({
Id: undefined,
SkillTree: undefined,
SkillTreeId: viewModel.currentSkillTree.entity.Id,
Skill: newSkill,
SkillId: undefined,
Position: { X: x, Y: y }
});
// Completing object-references
viewModel.currentSkillTree.entity.Elements.push(newSkillTreeElement);
newSkill.Levels.push(newSkillLevel);
newSkill.SkillTreeUsage.push(newSkillTreeElement)
newSkillLevel.RequirementSets.push(newRequirementSet);
// Saving
DataService.jaydata.saveChanges()
.then(function (cnt) {
console.log('Saved entities:', cnt);
// The cnt-result in console is 4
// But no request was executed, nothing was saved
}, function (exception) {
console.log(exception); // Also no exception was thrown
});
So to not be that unkind.
The solution to solve the problem above to me, since i tried nearly every combination with entities (adding, attaching, .save(), .saveChanges(), object-references etc, figuring out it doesn't make sense anyway, it just acted the same way and seems to be so buggy), ended up within a workaround acting with classic nested async calls.
The solution was to save entities seperately within nested promises and to turn off the batch behavior of jaydata, to avoid double requests.
You can find the option within $data.defaults
$data.defaults.OData.disableBatch = true;
As result i am dealing now with good old nasty pyramids of doom, which at least gave the possibility back to save entities in the right order, with full control, the way the api needs it.
// Saving new SkillLevelRequirement connection
if (isConnectionGiven === false) {
// The first level of source skill where the target-skill-requirement will be added
var sourceSkillLevel = Enumerable
.From(sourceSkill.Levels)
.FirstOrDefault(null, function (x) {
return x.Level === 1;
});
// The last level of the target-skill to solve
var targetSkillLevel = Enumerable
.From(targetSkill.Levels)
.FirstOrDefault(null, function (x) {
return x.Level === targetSkill.Levels.length;
});
// First set of first level from source skill (will be used to add skilllevel-requirement)
var firstRequirementSet = sourceSkillLevel.RequirementSets[0];
// New RequirementAsignment
var newRequirementAssignment = new DataService.jaydata.RequirementAssignments.elementType({
RequirementSetId: firstRequirementSet.Id,
Order: 1
});
// New Requirement
var newRequirement = new DataService.jaydata.Requirements.elementType({
Title: requirementTypes.SKILL_CONNECTION,
RequirementOfIntId: undefined,
RequirementOfBoolId: undefined,
RequirementOfSkillLevelId: 0
});
// New RequirementOfSkillLevel
var newRequirementOfSkillLevel = new DataService.jaydata.RequirementsOfSkillLevel.elementType({
SkillLevelId: targetSkillLevel.Id,
});
// Loading symbol
showBusyIndicator();
newRequirementOfSkillLevel.save()
.then(function () {
newRequirement.RequirementOfSkillLevelId = newRequirementOfSkillLevel.Id;
newRequirement.save()
.then(function () {
newRequirementAssignment.RequirementId = newRequirement.Id;
newRequirementAssignment.save()
.then(function () {
// Loading symbol will be closed after tree reloaded
reloadCurrentTree();
}, function (exception) {
showJayDataExceptionModal(exception);
});
}, function (exception) {
showJayDataExceptionModal(exception);
});
}, function (exception) {
showJayDataExceptionModal(exception);
});
}
}
#jaydata developers: Thanks for 42 new grey hairs. I'm still at the point where i think i am using your tool wrong and jaydata could do so much better. Better up your documentation, sieriously. No desserts for you today.

Failing To Check if Element is Disabled

I have a dropdown with elements that get disabled when conditions are met. In the test, I check them for being disabled, but all tests fail and always return the element state as enabled (Clearly incorrectly. I have ensured that this is not a timing issue - refreshed the page and gave ample wait time with browser sleep - the elements are clearly disabled on the screen). There is an anchor within a list item. Please see image:
I have tried checking both the list item and the anchor, like so:
var actionDropDownList = $$('[class="dropdown-menu"]').get(1);
var checkOutButtonState = actionDropDownList.all(by.tagName('li')).get(6);
actionsButton.click();
actionDropDownList.all(by.tagName('li')).count().then(function(count){
console.log('THE NUMBER OF ELEMENTS IN THE DROPDOWN IS...............................................................' + count);
}) //verify that I have the correct dropdown - yes
checkOutButtonState.isEnabled().then(function(isEnabled){
console.log('CHECKING checkOutButton BUTTON STATE: ' + isEnabled);
}) //log state - shows incorrectly
I have also tried checking the button itself for disabled state (the element below is what I tried checking instead of the list element):
var checkOutButton = $('[ng-click="item.statusId !== itemStatus.in || checkOut()"]');
This failed as well.
Not sure which one I should check and why both are failing. How do I correct this and get it to show that the disabled button is...well, disabled.
TEMPORARY ADD ON EDIT:
For simplicity's sake, I am trying:
var hasClass = function (element, cls) {
return element.getAttribute('class').then(function (classes) {
return classes.split(' ').indexOf(cls) !== -1;
});
var checkOutButtonState = actionDropDownList.all(by.tagName('li')).get(6);
expect(hasClass(checkOutButtonState, 'disabled')).toBe(true);
It still fails, however, despite the element clearly having the class. Alec - your solution throws "function is not defined," I am not sure if I need something else for it to see jasmine. Tried, but can't find anything wrong with it, not sure why I can't get it to work.
Edit:
If I run...since it only appears to have one class:
expect(checkOutButtonState.getAttribute('class')).toBe('disabled');
I get "expected 'ng-isolate-scope' to be 'disabled'"
In a quite similar situation I've ended up checking the presence of disabledclass:
expect(checkOutButtonState).toHaveClass("disabled");
Where toHaveClass() is a custom jasmine matcher:
beforeEach(function() {
jasmine.addMatchers({
toHaveClass: function() {
return {
compare: function(actual, expected) {
return {
pass: actual.getAttribute("class").then(function(classes) {
return classes.split(" ").indexOf(expected) !== -1;
})
};
}
};
},
});
});

Nested backbone model results in infinite recursion when saving

This problem just seemed to appear while I updated to Backbone 1.1. I have a nested Backbone model:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""},
parse: function (response) {
response.name = response.set_id;
response.problems = new ProblemList(response.problems);
return response;
}
});
var ProblemList = Backbone.Collection.extend({
model: Problem
});
I initially load in a ProblemSetList, which is a collection of ProblemSet models in my page. Any changes to the open_date or due_date fields of any ProblemSet, first go to the server and update that property, then returns. This fires another change event on the ProblemSet.
It appears that all subsequent returns from the server fires another change event and the changed attribute is the "problems" attribute. This results in infinite recursive calls.
The problem appears to come from the part of set method of Backbone.Model (code listed here from line 339)
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = true;
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
The comparison on the problems attribute returns false from _.isEqual() and therefore fires a change event.
My question is: is this the right way to do a nested Backbone model? I had something similar working in Backbone 1.1. Other thoughts about how to proceed to avoid this issue?
You reinstantiate your problems attribute each time your model.fetch completes, the objects are different and thus trigger a new cycle.
What I usually do to handle nested models:
use a model property outside of the attributes handled by Backbone,
instantiate it in the initialize function,
set or reset this object in the parent parse function and return a response omitting the set data
Something like this:
var ProblemSet = Backbone.Model.extend({
defaults: {
name: "",
open_date: "",
due_date: ""
},
initialize: function (opts) {
var pbs = (opts && opts.problems) ? opts.problems : [];
this.problems = new ProblemList(pbs);
},
parse: function (response) {
response.name = response.set_id;
if (response.problems)
this.problems.set(response.problems);
return _.omit(response, 'problems');
}
});
parse gets called on fetch and save (according to backbone documentation), this might cause your infinite loop. I don't think that the parse function is the right place to create the new ProblemsList sub-collection, do it in the initialize function of your model instead.

Resources