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/
Related
I have the following backbonejs code which fetches data in collection from my API.
app.js
var app = app || {};
app.Book = Backbone.Model.extend({
defaults: {
photo: 'https://tpc.googlesyndication.com/simgad/10102369525962162085'
}
});
app.Library = Backbone.Collection.extend({
model: app.Book,
url: 'http://localhost:8000/api/v1/products_user',
parse: function (response) {
return response.data;
}
});
app.BookView = Backbone.View.extend({
tagName: 'article',
className: 'white-panel',
template: _.template( $( '#product_grid_template' ).html() ),
render: function() {
//this.el is what we defined in tagName. use $el to get access to jQuery html() function
this.$el.html( this.template( this.model.attributes ) );
return this;
}
});
app.LibraryView = Backbone.View.extend({
el: '#pinBoot',
initialize: function() { // UPDATED
this.collection = new app.Library(); // UPDATED
this.collection.fetch({
reset:true});
this.listenTo( this.collection, 'add', this.renderBook );
this.listenTo( this.collection, 'reset', this.render ); // NEW
},
// render library by rendering each book in its collection
render: function() {
console.log('render');
this.collection.each(function( item ) {
this.renderBook( item );
}, this );
},
// render a book by creating a BookView and appending the
// element it renders to the library's element
renderBook: function( item ) {
var bookView = new app.BookView({
model: item
});
this.$el.append( bookView.render().el );
}
});
new app.LibraryView();
The above code renders the following HTML
<article class="white-panel r1 c0" style=
"width: 316.667px; left: 0px; top: 0px;">
<div class="widget portfolio graphics homepage">
<div class="entry-container span4">
<!-- Portfolio Image -->
<div class="entry-image">
<a href="product.html"><img alt="" src=
"https://tpc.googlesyndication.com/simgad/10102369525962162085"></a>
</div>
<div class="entry drop-shadow curved">
<!-- Portfolio Heading -->
<div class="heading">
My product HII
</div>
<div class="camera" data-target="#prod0-modal" data-toggle=
"modal">
3
</div>
<div class="price">
₹ 10 <span>+ Delivery Charges ₹ 11</span>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
</article>
The whole HTML and data was getting rendered to the HTML and I found the why the was not visible was because, a style parameter 'style="opacity: 1;"' is not getting added to the <div class="entry-container span4">
The above app.js , I have modified to work, without an API as follows
app.LibraryView = Backbone.View.extend({
el: '#pinBoot',
initialize: function(initialBooks) {
// this.collection = new app.Library();
// this.collection.fetch({
// reset:true});
// this.listenTo( this.collection, 'add', this.renderBook );
// this.listenTo( this.collection, 'reset', this.render ); // NEW
this.collection = new app.Library( initialBooks );
this.render();
},
// render library by rendering each book in its collection
render: function() {
console.log('render');
this.collection.each(function( item ) {
this.renderBook( item );
}, this );
},
// render a book by creating a BookView and appending the
// element it renders to the library's element
renderBook: function( item ) {
var bookView = new app.BookView({
model: item
});
this.$el.append( bookView.render().el );
}
});
var books = [
{ product_name: 'My product',delivery_charge: '10.00',amount:'10.22' },
];
new app.LibraryView(books);
Now everything works perfectly, the style gets added to the div without any problems. The style is getting added using a fade effect javascript which is suppose to give a fadein effect to the grids, but when I fetch data and renders using the API , the div doesn't get added by the style element.
Why is this happening? Is it something to do with the pageload rendering or asynchronous data loading?
I am new to backbonejs, can someone helps me whats going wrong here when I call the API?
UPDATE
I have this script which is adding the opacity element to the div for visiblity
<script type="text/javascript">
//<![CDATA[
$(window).load(function() { // makes sure the whole site is loaded
$("#status").fadeOut(); // will first fade out the loading animation
$("#preloader").delay(350).fadeOut("slow"); // will fade out the white DIV that covers the website.
$("#preloader-container").css("position","static");
$(".widget .entry-container").each(function(index) {
$(this).delay(400*index).animate({'opacity': 1},300);
});
});
//]]>
</script>
Is this script conflicting with my backbone data rendering?
You are fetching your data before you bind the event for handling it. If the fetch ist fast enough this might cause a race condition.
initialize: function() { // UPDATED
this.collection = new app.Library(); // UPDATED
this.collection.fetch({
reset:true});
this.listenTo( this.collection, 'add', this.renderBook );
this.listenTo( this.collection, 'reset', this.render ); // NEW
}
After setting up the views collection, bind all the necessary events and then do anything with it:
initialize: function() { // UPDATED
this.collection = new app.Library(); // UPDATED
this.listenTo( this.collection, 'add', this.renderBook );
this.listenTo( this.collection, 'reset', this.render ); // NEW
this.collection.fetch({
reset:true});
}
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.
I'm trying to add a new item to my backbone collection using:
window.bearList.create({ name: "Tina" } );
This correctly saves the new item to the server, because afterwards I can see this on the server, which is what I want. (I'm using MongoDB)
{"name":"Tina","_id":"53b41d92b7083d0b00000009","__v":0}
I have this binding in my bearList ListView:
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('add', this.render);
},
The problem is that the code above just adds the following to my collection view until I reload the page.
{"name":"Tina"}
I've tried using the model.save() callback, but I still have the same issue.
Like I said, everything looks fine on the server, and the collection has the correction version of 'Tina' once I reload the page.
But for some reason, it is not getting the full model for the ListView's 'render' event. I've tried fetching each model individually on the ListView render method, but this did not work and is bad practice anyway.
Can someone help me out?
Here is my full code for this:
window.ListView = Backbone.View.extend({
tagName: 'ul',
className: 'list-group',
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('add', this.render);
},
render: function(){
this.$el.html(" ");
this.collection.each(function(item){
var listItemView = new ListItemView({ model: item });
this.$el.append(listItemView.render().el);
}, this);
return this;
},
});
window.ListItemView = Backbone.View.extend({
tagName: 'li',
className: 'list-group-item',
initialize:function () {
this.model.bind("change", this.render);
},
render:function () {
console.log(JSON.stringify(this.model.toJSON()));
this.$el.html("<a href='#"+ this.model.hashType + "/"+this.model.get('_id')+"' >" + this.model.get('name') + "</a>");
return this;
}
});
Just pass wait:true.
window.bearList.create({ name: "Tina" }, {wait: true} );
http://backbonejs.org/#Collection-create
Pass {wait: true} if you'd like to wait for the server before adding
the new model to the collection.
Listen to the sync event since add will only add the values you are creating from within backbone. http://backbonejs.org/#Sync
And a tip: use listenTo to use more of Backbone's features.
instead of
initialize:function () {
this.model.bind("change", this.render);
},
use:
initialize:function () {
this.listenTo( this.model, "change", this.render );
},
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.
full code here... http://pastebin.com/EEnm8vi3
line 378 is not inserting the sections into the current view. the section model is correctly being passed into the method. everything else works as expected except for the insertion of the child rendered views.
I am wanting to know why $(this.el) is empty and therefore not allowing an append. trying to use a direct selector like $('#sections') also does not work.
relevent code repeated from pastbin link above: (addOne method)
SCC.Views.SectionView = Backbone.View.extend({
tagName: "div",
className: "section",
initialize: function(){
_.bindAll(this, 'render');
this.template = _.template($('#section-tmpl').html());
},
render: function() {
console.log($(this.el));
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
SCC.Views.SectionsView = Backbone.View.extend({
tagName: "div",
id: "sections",
className: "sections",
initialize: function(){
_.bindAll(this, 'render');
//SCC.Sections.bind('add', this.addOne, this);
SCC.Sections.bind('reset', this.addAll, this);
SCC.Sections.bind('all', this.render, this);
},
render: function() {
$(this.el).html("<p>rendered</p>");
return this;
},
addOne: function(section) {
var view = new SCC.Views.SectionView({model: section});
$(this.el).append(view.render().el);
},
addAll: function() {
this.collection.each(this.addOne);
}
});
SCC.Sections = new SCC.Collections.Sections();
SCC.SectionsView = new SCC.Views.SectionsView({collection:SCC.Sections});
SCC.Sections.reset(window.SectionData);
$('#main').append(SCC.SectionsView.render().el);
I ran into this problem myself and so I'll leave this answer for anyone else out there:
When you bind this.render to 'all' as #lukemh did:
SCC.Sections.bind('all', this.render, this);
You're effectively saying everytime an event is triggered in the model/collection re-render the view. When you use .html() in that render method you're also going to override any child views that may have been .append()'ed to it throught the addOne function.
If you move the $(this.el).html() call to the initialize view the problem is solved. You can still bind render to 'all' but make sure you're only re-rendering a portion of it or else you'll override the child views again.