My server-side api follows a classic results-paging model, e.g.
/api/transactions/ => page 1 (10 items default limit)
/api/transactions/?p=2 => page 2
I want to build a infinite-scrolling system with Backbone views.
I already have non-paging collection+views setup. The parent view looks like this:
Backbone.View.extend({
initialize: function() {
this.collection = TransactionCollection;
this.fetch();
this.listenTo( this.collection, 'reset', this.renderEntries );
this.listenTo( this.collection, 'add', this.fetch );
this.rowViews = [];
this.render();
},
fetch: function() {
this.collection.fetch({ reset:true });
},
render: function() {
this.$el.html( template() );
this.renderEntries();
return this;
},
renderEntries: function() {
this.clearEntryRows();
this.collection.each(function(item) {
var row = new TransactionItemView( item );
this.rowViews.push( row );
this.$el.find('.entry-list').append( row.render().el );
}, this);
},
clearEntryRows: function() {
this.rowViews.forEach(function(v) {
if (v.close) v.close();
});
this.rowViews = [];
},
// ...
}
This is the relevant part of the view code (child views are simple item views, rendering themselves with a template).
The collection is still very basic:
var TransactionCollection = Backbone.Collection.extend({
model: Transaction,
url: '/api/transactions'
});
Now it's time to add pagination. I think I'm going to add a button "MORE...", after each renderEntries() call. That button will fetch for the next page (without resetting the collection) and another renderEntries is called. The this.clearEntryRows() will be moved to the reset callback.
My question is: how can I fetch the second page and add models without resetting the collection and intercept just that event, to render next entry pages?
I've read something about 'sync' event: in my understanding, 'reset' gets fired only when I fetch with reset:true, 'sync' gets fired every time I fetch the collection, anyway.
So, if this is correct, I can clear entry rows only on reset event and display rows in sync. But how can I display only the newly added (e.g. page 2) rows to my list?
I'm a little confused.
this.collection.fetch({ add: true, remove: false, merge: false, data: {p: 2} });
this allow you to fetch with specified get parameters and only add not existing entries to collection.
In your view:
initialize: function () {
this.listenTo(this.collection, 'add', handlerForRenderingNewEntries);
}
To render only new models, you can return them with specified attribute, extra property 'page' for example. Filter them by this attribute and send to rendrer.
Related
I'm trying to use the event aggregator to fire a method off of a model's view. The problem is, when I fire the update or save method for the ItemView, it iterates through all models in the collection. How do I get it to not only fire properly for the model which the view represents (or a new modal in the save method's case), but also prevent it from firing for every model in the collection?
This application consists of a collection of Items, each Item has a model which is rendered into an ItemView and listed on the page. If a user clicks the edit item icon, then a ModalView is instantiated and the current Item model data is injected into the ModalView.
The ModalView which gets loaded with a template for the respective task. For this instance, i'm loading a template to edit the Item. Here's a summary of the relevant code:
var ModalView = Backbone.View.extend({
tagName: "section",
className: "modal",
events: {
'click .close': 'close',
'click .minimize': 'minimize',
'click .maximize': 'maximize',
'click .save-item': 'saveItem',
},
html: null,
initialize: function(options) {
this.template = _.template(ModalTemplate);
this.vent = options.vent;
},
saveItem: function() {
this.vent.trigger('item.save');
},
});
The item collection's view is here:
var ItemsView = Backbone.View.extend({
tagName: 'ul',
className: 'item-items',
render: function(){
var self = this;
// Make it flex GRRRR
this.$el.addClass('flex-item');
this.collection.each(function(item) {
var date = item.get('created_at');
var itemView = new ItemView({ model: item, vent: App.vent });
this.$el.append(itemView.render().el);
}, this);
return this;
}
});
Finally, the item model's view which contains the edit method that fires the ModalView
var ItemView = Backbone.View.extend({
tagName: 'li',
className: 'item',
events: {
'click .edit-item': 'edit'
},
initialize: function(options) {
this.template = _.template(ItemTemplate);
options.vent.bind("item.save", this.save);
options.vent.bind("item.update", this.update);
},
save: function() {
var attributes, item;
item = new App.api.item.model();
attributes = getMeta(true);
item.save(attributes)
.done(function(res) {
Ui.modal.destroy();
// Re-render items
App.routers.main.render.User.sidebar();
App.routers.main.render.Item.items(function() {
Ui.resizeContent();
});
})
.fail(function(res) {
console.log(res);
});
},
update: function() {
console.log('update') // fires App.api.item.collection.length times
var attributes, item;
item = App.api.item.collection.get(App.rendered.modal.$el.data('id'));
attributes = getMeta();
item.save(attributes)
.done(function(res) {
Ui.modal.destroy();
// Re-render items
App.routers.main.render.Item.items(function() {
Ui.resizeContent();
});
})
.fail(function(res) {
console.log(res);
});
},
edit: function() {
Ui.modal.new(ItemModalTemplate, this.model.attributes);
App.rendered.modal.$el.attr('data-id', this.model.get('_id'));
// New Editor
var editor = document.querySelector('#item-editor');
window.editor = new MediumEditor(editor, editorOptions);
}
});
Obviously I'm missing something fundamental here because console.log('update') in the save method of the ItemView fires for every item in the collection. What I was trying to do was keep the logic for save, and update in the view for the Item for organizational purposes.
Many thanks.
Instead of options hold the model itself in the ItemModelView so you can call save directly without need for events.
Replace this Ui.modal.new(ItemModalTemplate, this.model.attributes); with UI.modal.new(ItemModalTemplate, this.model), and this this.vent.trigger('item.save'); with this.model.save()
I'm having difficulty getting a model with an auto-incrementing "order" attribute working in BackboneJS.
For some reason every order gets set to 1. The length of the collection in the nextOrder function is always 0.
Options = _.extend(Options, {
Models: {
Profile: Backbone.Model.extend({
defaults: function() {
console.log("Defaults");
return {
title: "New Profile",
order: Profiles.nextOrder(),
active: false
};
},
url: "/youdontcare"
})
});
Options = _.extend(Options, {
Collections: {
ProfileList: Backbone.Collection.extend({
model: Options.Models.Profile,
comparator: function(profile) {
console.log("Comparator");
return profile.get('order');
},
nextOrder: function() {
console.log("nextOrder...");
console.log(this.length);
if (!this.length) return 1;
return this.last().get('order') + 1;
},
url: "/youdontcare"
})
});
Options = _.extend(Options, {
Views: {
ProfileView: Backbone.View.extend({
tagName: "li",
template: _.template($('#profile-template').html()),
render: function() {
console.log("Render Profile");
this.$el.html(this.template(this.model.toJSON()));
return this;
}
}),
ProfileListView: Backbone.View.extend({
el: $("#auth_env_list"),
initialize: function() {
Profiles = new Options.Collections.ProfileList;
console.log("INIT LIST");
this.listenTo(Profiles, 'add', this.addOne);
this.listenTo(Profiles, 'reset', this.addAll);
this.listenTo(Profiles, 'all', this.render);
// Suppresses 'add' events with {reset: true} and prevents the app view
// from being re-rendered for every model. Only renders when the 'reset'
// event is triggered at the end of the fetch.
console.log("Fetching");
Profiles.fetch({ reset: true });
console.log("Done fetching");
},
addOne: function (profile) {
console.log("addOne");
var view = new Options.Views.ProfileView({ model: profile });
this.$el.append(view.render().el);
},
addAll: function () {
console.log("addAll");
this.$el.html('');
Profiles.each(this.addOne, this);
},
render: function() {
console.log("RENDER PROFILE LIST VIEW");
if (Profiles.length)
{
}
}
})
});
I can see that the nextOrder function inside the Profiles instance of the Options.Collections.ProfileList collection is called the appropriate number of times for each element that is fetched for the collection... however the length of the collection it tries to compute with this.length always returns 0!
Console output with 5 "Profile" elements:
INIT LIST
Fetching
RENDER PROFILE LIST VIEW
Done fetching
Defaults
nextOrder...
0
Defaults
nextOrder...
0
Defaults
nextOrder...
0
Defaults
nextOrder...
0
Defaults
nextOrder...
0
Comparator
addAll
addOne
Render Profile
addOne
Render Profile
addOne
Render Profile
addOne
Render Profile
addOne
Render Profile
RENDER PROFILE LIST VIEW
RENDER PROFILE LIST VIEW
Is there a better way I could assign an auto incrementing client side ID to these? The only reason I want to do it is to display them in a numbered list.
By calling the collection via the model, you're kind of creating a circular reference, and it's not very efficient if you were going to reuse the code on a different collection. It's possible that you're getting 0 back because it's not referring to the actual collection instance. A better way to accomplish what you want would be to have the collection assign the order number whenever a new model is added to the collection:
// 1st, get rid of the adding an order in your model
// in your collection, add something like the following
initialize: function() {
this.on('add', this.addOrderID);
this.on('reset', this.addOrderIDs);
},
addOrderID: function(model) {
var order = this.length;
model.set({'order': order});
},
addOrderIDs: function() {
var order = this.length;
this.models.forEach( function(model) {
model.set({'order': order});
order++;
}, this);
}
I think that should accomplish what you're looking for.
I want to show a loading message/icon until all the items in the list have been rendered.
Here is the jsfiddle with my example: http://jsfiddle.net/9R9zU/58/
I've tried to add a div with a loading bar in the Feed section, but it doesn't work.
How can I show a loading message before all the book views are rendered in the book list view:
app.BookListView = Backbone.View.extend({
el: '.feed',
initialize: function() {
this.render();
this.listenTo( this.collection, 'add', this.renderBook );
In theory you need to fetch some content asynchronously from somewhere to display the loader. A loading is needed to show the user that you are actually fetching the content and that the UI is not dead.
In that fiddle even if you got it working you won't be able to see it because the collection is bootstrapped and you are not fetching anything.
This simulates that (updated your fiddle):
app.BookListView = Backbone.View.extend({
el: '.feed',
initialize: function() {
this.loader();
this.listenToOnce( this.collection, 'sync', this.render); // or listenTo ?
this.listenTo( this.collection, 'add', this.renderBook );
// this simulates the fetching...
// It's not really needed
var self = this;
setTimeout(function(){
self.collection.trigger('sync');
}, 3000)
},
loader: function(){
this.$el.html('<div>Loading...</div>')
},
render: function() {
this.$el.empty();
this.collection.each(function( item ){
this.renderBook( item );
}, this);
},
renderBook: function ( item ) {
var bookview = new app.BookView ({
model: item
});
this.$el.append( bookview.render().el );
}
});
Here's a working example: http://jsfiddle.net/aJfUx/1/
render: function() {
// Make this loading icon/message whatever you want
this.$el.html("<i class='icon-spin icon-refresh loading-icon' />");
this.collection.each(function( item ){
this.renderBook( item );
}, this);
this.$el.find(".loading-icon").remove();
}
And here's an example that uses setTimeout to artificially add some loading time so you can see that spinner spin!
http://jsfiddle.net/7ddXM/
I am working on a contact bar which renders all contacts of a user in a html list.
What I have:
UserModel - This is a simple Backbone.Model with username and email
UserCollection - This is used as the contact list
ContactsView - This is the ul contact list
ContactView - This is a single contact model rendered as li
I am currently breaking my head about a solution how (and where) I can fetch my UserCollection and how I pass the single models down to a single ContactView item.
Specific hurdles are:
Where should I fetch, store the UserCollection
How do I render the contact list
How do I render the contact items
How do I prevent fetch({ success: callback }) from breaking my code structure
My current code is this:
entrance point:
// create a new instance of the contact list view
var view = new ContactsView();
// insert the rendered element of the contact list view in to the dom
$('div.contacts-body').html(view.render().el);
view.fetch({ success: view.loadContacts });
ContactsView:
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contacts.html', 'collections/users', 'views/conversations/contact'],
function($, _, Backbone, ContactsTemplate, UserCollection, ContactView) {
var ContactsView = Backbone.View.extend({
tagName: "ul",
className: "contacts unstyled",
attributes: "",
// I am feeling uneasy hardcoding the collection into the view
initialize: function() {
this.collection = new UserCollection();
},
// this renders our contact list
// we don't need any template because we just have <ul class="contacts"></ul>
render: function() {
this.$el.html();
return this;
},
// this should render the contact list
// really crappy and unflexible
loadContacts: function() {
this.collection.each(function(contact) {
// create a new contact item, insert the model
var view = new ContactView({ model: contact });
// append it to our list
this.$el.append(view.render().el);
});
}
});
return ContactsView;
});
ContactView
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contact.html'],
function($, _, Backbone, ContactTemplate) {
var ContactView = Backbone.View.extend({
tagName: "li",
className: "contact",
attributes: "",
template:_.template(ContactTemplate),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
return ContactView;
});
Could somebody help me about my four hurdles.
Good example links are welcome. I oriented my code style at the todos list unfortunatly the todos list isn't that advanced...
UPDATED CODE:
define(
['jquery', 'underscore', 'backbone', 'text!templates/conversations/contacts.html', 'collections/users', 'views/conversations/contact'],
function($, _, Backbone, ContactsTemplate, UserCollection, ContactView) {
var ContactsView = Backbone.View.extend({
tagName: "ul",
className: "contacts unstyled",
attributes: "",
events: {
},
initialize: function() {
this.collection = new UserCollection();
this.collection.on('reset', this.render);
this.collection.fetch();
},
render: function() {
// in chromium console
console.log(this.el); // first: html, second: undefined
console.log(this.$el); // first: html in array, second: undefined
this.$el.empty(); // error on the called that this.$el is undefined
this.collection.each(function(contact) {
var view = new ContactView({ model: contact });
this.$el.append(view.el);
}.bind(this));
return this;
}
});
return ContactsView;
Can it be that reset is triggering this.render twice?
First of all: why do you fetch the view? Backbone views do not have a fetch method..
1 The correct place to fetch your UserCollection would be inside the view's initialize method:
initialize: function() { // ContactsView
_.bindAll(this, 'render', 'otherMethodName', ...); // Bind this to all view functions
...
this.collection.on('reset', this.render); // bind the collection reset event to render this view
this.collection.fetch();
...
}
Now you fetch the contacts exactly when you need them. Next step is to render the collection.
2 Binding to the reset event makes your loadContacts method obsolete and we can do that in the render function:
render: function() {
this.$el.empty(); // clear the element to make sure you don't double your contact view
var self = this; // so you can use this inside the each function
this.collection.each(function(contact) { // iterate through the collection
var contactView = new ContactView({model: contact});
self.$el.append(contactView.el);
});
return this;
}
Now you render your contactlist inside the render method, where it should be done.
3 The ContactView actually looks good.
Just make the item to render itself in the initialize method, so you don't have to make useless calls in the ContactsView's render method and clutter up your code. Also bindAll here as well.
initialize: function() { // ContactView
_.bindAll(this, 'render', 'otherMethodName', ...);
...
this.render(); // Render in the end of initialize
}
I have no idea what you are asking in here, but I think the best way is not to use success callbacks. The collections and models trigger events whenever something is done to them, so tapping onto them is much more robust and reliable than success callbacks. Check out the catalog of events to learn more. The Wine Cellar tutorial by Christophe Coenraets is has an excellent example of this kind of listview-listitemview arrangement.
Hope this helps!
UPDATE: Added _.bindAlls to fix the problem with this in a event bound render call. Some info on binding this.
NOTE: all the code is simplified and no tested
When I have all the elements structure defined, as you have, with all the Models, Collections and Views implemented then I implement a Loader which is in charge of trigger the fetching and rendering actions.
First of all I need to expose the classes definition from the outside something like this:
// App.js
var App = {}
// ContactsCollection.js
$(function(){
var App.ContactsCollection = Backbone.Collection.extend({ ... });
});
// ContactsView.js
$(function(){
var App.ContactsView = Backbone.View.extend({ ... });
});
// and so on...
And then I implement what I call the Loader:
// AppLoad.js
$(function(){
// instantiate the collection
var App.contactsCollection = new App.ContactsCollection();
// instantiate the CollectionView and assign the collection to it
var App.contactsView = new App.ContactsView({
el: "div.contacts-body ul",
collection: App.contactsCollection
});
// fetch the collection the contactsView will
// render the content authomatically
App.contactsCollection.fetch();
});
Another changes you have to do is configure the ContactsView in a way that respond to the changes in the App.contactsCollection because as the fetch() is asynchronous you can call render() when the collection is still not loaded, so you have to tell to the CollectionView to render it self when the Collection is ready:
var ContactsView = Backbone.View.extend({
initialize: function( opts ){
this.collection.on( 'reset', this.addAll, this );
this.collection.on( 'add', this.addOne, this );
// ... same with 'remove'
},
addOne: function( model ){
var view = new App.ContactView({ model: contact });
this.$el.append( view.render().el );
},
addAll: function(){
this.collection.each( $.proxy( this.addOne, this ) );
}
});
You have to require your js files in the proper order:
App.js
Your Models, Collections, Views
AppLoad.js
With this system you obtain:
External access to your collection in case you need to access it from another place.
External control of the CollectionView.el with is better for decoupling and testing.
The CollectionView will respond to changes in the Collection authomatically
Note: If you use Router you can move the AppLoad.js logic to there.
When a user clicks on a div with class .photo_container which is part of the view PhotoListView, there is a function sendSelectedPhotoId that will be triggered. This function has to get the attribute photo_id from the Photo model that belongs to this view whose div .photo_container element has been clicked, and send it to the serverside via fetch().
Problem: So far I managed to get the function sendSelectedPhotoId to be triggered when the div is clicked, but I cant figure out how to get the photo_id attribute of the view's Photo model. How should I achieve this?
On a side note, I'm not sure whether the correct photo_id will be send.
Code
$('#button').click( function() {
// Retrieve photos
this.photoList = new PhotoCollection();
var self = this;
this.photoList.fetch({
success: function() {
self.photoListView = new PhotoListView({ model: self.photoList });
$('#photo_list').html(self.photoListView.render().el);
}
});
});
Model & Collection
// Models
Photo = Backbone.Model.extend({
defaults: {
photo_id: ''
}
});
// Collections
PhotoCollection = Backbone.Collection.extend({
model: Photo,
url: 'splash/process_profiling_img'
});
Views
// Views
PhotoListView = Backbone.View.extend({
tagName: 'div',
events: {
'click .photo_container': 'sendSelectedPhotoId'
},
initialize: function() {
this.model.bind('reset', this.render, this);
this.model.bind('add', function(photo) {
$(this.el).append(new PhotoListItemView({ model: photo }).render().el);
}, this);
},
render: function() {
_.each(this.model.models, function(photo) {
$(this.el).append(new PhotoListItemView({ model: photo }).render().el);
}, this);
return this;
},
sendSelectedPhotoId: function() {
var self = this;
console.log(self.model.get('photo_id'));
self.model.fetch({
data: { chosen_photo: self.model.get('photo_id')},
processData: true,
success: function() {
}});
}
});
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
template: _.template($('#tpl-PhotoListItemView').html()),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.close, this);
},
render: function() {
$(this.el).html(this.template( this.model.toJSON() ));
return this;
},
close: function() {
$(this.el).unbind();
$(this.el).remove();
}
});
SECOND ATTEMPT
I also tried placing the event handler and sendSelectedPhotoId in the PhotoListItemView where I managed to get the Model's attribute properly, but I can't figure out how to trigger the reset event when the PhotoList collection did a fetch().
View
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
events: {
'click .photo_container': 'sendSelectedPhotoId'
},
template: _.template($('#tpl-PhotoListItemView').html()),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.close, this);
},
render: function() {
$(this.el).html(this.template( this.model.toJSON() ));
return this;
},
close: function() {
$(this.el).unbind();
$(this.el).remove();
},
sendSelectedPhotoId: function() {
console.log('clicked!');
var self = this;
console.log(self.model.get('photo_id'));
self.model.fetch({
data: { chosen_photo: self.model.get('photo_id')},
processData: true,
success: function() {
$(this.el).html('');
}});
}
});
Problem: With this, I cant seem to fire the reset event of the model after doing the fetch() in function sendSelectedPhotoId, which means I cant get it to re-render using PhotoListView's render().
In the screenshot below from Chrome's javascript console, I printed out the collection after sendSelectedPhotoId did its fetch(), and it seems like the fetched added the new data to the existing model, instead of creating 2 new models and removing all existing model!
You already have child views for each model, so I would put the click event handler in the child view. In the handler in the child, trigger an event passing this.model, and listen for that event in your parent.
Update based on update:
Try changing
this.model.bind('reset', this.render, this); to
this.model.bind('remove', this.render, this); // model is a collection right?
and then remove the model from the collection after the view is clicked. Also, I don't think using Model.fetch is what you really want to do. Maybe a .save or a custom method on the model?
Update based on author's comment showing sample base from blog
I would not follow that blog's advice. If you are using backbone professionally I can't recommend the Thoughtbot ebook enough.
It's $50 for a work in progress, and it's worth every penny
It has a simple sample application that lays out how to organize a backbone app. This is why I bought the book.
It uses Rails in the examples for the backend, but I have used Rails, Node, and C# MVC and all work no problem.