My ultimate goal is to append the JSON data to ul#tweets, each as individual hidden list items. They will then, one by one over time, become visible/shown on the screen, and then be removed from the ul#tweets list.
Once the number of hidden items drops below a certain amount, I want to re-append the JSON data. When this happens, I am not worried about duplicate items.
I tried to setup a test by creating a function with a timeout so that every 5 seconds it would append the JSON data to the list.
However, though my app loads the initial data on pageload fine, when I create a function to be run within $(document).ready({}) - it won't work.
I do know, however, that I can append the JSON data manually in the console after page load (same code as below without wrapping it in the function or the doc.ready).
Thanks for the help!
Function:
$(document).ready(function(){
updateTweets = function() {
newTweets = new Tweets();
newTweets.fetch();
newTweets.each( function(tweet) {
console.log('test'); // this doesn't work
view = new TweetView({ model:tweet });
$('#tweets').append(view.render().el);
});
setTimeout(updateTweets, 5000);
};
updateTweets();
});
Here is my Code
// MODEL
window.Tweet = Backbone.Model.extend({});
// COLLECTION
window.Tweets = Backbone.Collection.extend({ model: Tweet, url: '/tweets' });
// SET GLOBAL VARIABLE FOR NEW TWEETS COLLECTION
window.tweetList = new Tweets();
$(document).ready(function() {
// MODEL VIEW
window.TweetView = Backbone.View.extend({
tagName: 'li',
className: 'tweet',
initialize: function() {
_.bindAll(this, 'render');
this.model.bind('change', this.render);
this.template = _.template($('#tweet-template').html());
},
render: function(){
var renderedTweets = this.template(this.model.toJSON());
$(this.el).html(renderedTweets);
return this;
}
});
// COLLECTION VIEW
window.TweetListView = Backbone.View.extend({
template: _.template($('#tweet-list-template').html()),
initialize: function() {
_.bindAll(this, 'render');
this.collection.bind('reset', this.render);
},
render: function() {
var $tweets,
collection = this.collection;
$(this.el).html(this.template({}));
$tweets = this.$('#tweets');
collection.each(function(tweet){
var view = new TweetView({
model: tweet,
collection: collection
});
$tweets.append(view.render().el);
});
return this;
}
});
// ROUTER
window.TweetListDisplay = Backbone.Router.extend({
routes: {
'': 'home'
},
initialize: function(){
this.tweetListView = new TweetListView({
collection: window.tweetList
});
},
home: function() {
var $container = $('#container');
$container.empty();
$container.append(this.tweetListView.render().el);
},
});
// DECLARE AND START APP
window.app = new TweetListDisplay();
Backbone.history.start();
}); // close $(document).ready({});
You call fetch here
newTweets.fetch();
And then right after start processing the collection as if it has been populated, here
newTweets.each( function(tweet) {
console.log('test'); // this doesn't work
view = new TweetView({ model:tweet });
$('#tweets').append(view.render().el);
});
fetch is an ASYNCHRONOUS operation, which means that after you fire it, the rest of the program will continue to execute immediately after, regardless if the ajax-call launched by the fetch has returned or not. So when you start processing the collection, your fetch hasn't yet returned and the collection is still empty.
There are 2 ways you can correct this situation. Let's start by making a function processCollection that does to the collection exactly what you want:
var processCollection = function () {
newTweets.each( function(tweet) {
console.log('test'); // this doesn't work
view = new TweetView({ model:tweet });
$('#tweets').append(view.render().el);
});
};
1 The callback function (I don't like these)
newTweets.fetch(success: processCollection);
Now processCollection will be called right after the fetch has succeeded.
2 Bind to events (I prefer this)
newTweets.on('reset', processCollection);
newTweets.fetch();
When the fetch returns successfully, it will populate the collection and fire a reset -event. This is a good place to tie your processing event, because you know that now the collection is populated. Also I find that there is slightly less scoping problems with events than with callbacks.
Hope this helps!
You cant call;
newTweets.fetch();
And then immediately start processing the collection as if its ready to use.. it takes time.. the fetch call is asynchronous.. the reason it works in console is that it takes time to prep the output for console.. and the fetch does indeed finish..
You should provide a success callback for the fetch like this:
newTweets.fetch({success: function(){//process collection}});
Related
I want to perform an action, clearing parent element, after a collection has fetched his models but prior to the models rendering.
I've stumbled upon before and after render methods yet they are model specific, which will cause my parent element to clear before every model rendering.
I'm able of course to perform the action pre-fetching yet I want it to occur when fetch is done and before models are rendered.
I tried using reset and change events listening on the collection yet both resulted unwanted end result.
Reset event seamed to go in that direction yet the passed argument was the entire collection and not a single model from the collection, therefore using the add event callback wasn't possible due to difference in argument type (collection and not a model as required)
Any ideas how to invoke a callback when fetch a collection fetch is successful yet models are yet to be rendered?
The model contains the returned attributes while collection contains url for fetching and parse method to return argument wrapped object.
Below is the code I use to render the collection view, which is basically rendering each model's view within the collection.
Collection View
---------------
var FoosView = Backbone.View.extend({
el: '#plans',
events: {
//'click tr': 'rowClick'
},
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
_.bindAll(this, "render");
this.render();
},
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
...
});
The model view
--------
var FooView = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.options = options || {};
this.tpl = _.template(fooTpl);
},
render: function() {
var data = this.model.toJSON();
this.$el.html(this.tpl(data));
return this;
}
});
Thanks in advance.
OK, I think I understand your question and here is a proposed solution. You are now listening to the reset event on your collection and calling this.renderAll. this.renderAll will take the list of models from the collection and render them to the page, but only AFTER the list element has been emptied. Hope this helps :).
var FoosView = Backbone.View.extend({
el: '#plans',
collection: yourCollection, // A reference to the collection.
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
this.listenTo(this.collection, 'reset', this.renderAll);
},
renderAll: function() {
// Empty your list.
this.$el.empty();
var _views = []; // Create a list for you subviews
// Create your subviews with the models in this.collection.
this.collection.each(function(model) {
_views.push(new FooView({model: model});
});
// Now create a document fragment so as to not reflow the page for every subview.
var container = document.createDocumentFragment();
// Append your subviews to the container.
_.each(_views, function(subview) {
container.appendChild(subview.render().el);
});
// Append the container to your list.
this.$el.append(container);
},
// renderNew will only run on the collections 'add' event.
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
});
I am forced to assume a few things about you html, but I think the above code should be enough to get you up and running. Let me know if it works.
I'm not totally sure about what you are asking but have you tried:
MyCollection.fetch({
success: function(models,response) {
//do stuff here
}
});
Also you may be interested taking a look at http://backbonejs.org/#Model-parse
Hope it helps!
Edit: there is no direct link between fetching and rendering my bet is that you binded rendering to model change.
USE===============>>>> http://backbonejs.org/#Model-parse
I have a single page app built with Marionette that has a main view with a list of subviews.
The JSON which holds all application data is updated constantly. I've tried to separate region show code so that it will be run just once and not on every render.
Now the render event is fired on every timeout loop even though the JSON is static data and therefore change event should not call render. What is wrong? I assume it has something to do with .set but is there any other way to load the response from an array variable to the subview collection, since fetch will allow only url attribute and will not accept array variable?
This example is an extremely simplified version of the application code to concentrate on this specific problem.
Controller:
var Controller = {
showMainView: function(id){
// create model and fetch data on startup
var mainElement = new mainElement();
var mainElementFetched = mainElement.fetch({url: 'http://json.json'});
// fetch done, create view, show view in region, setTimeout
mainElementFetched.done(function(data){
var mainElementView = mainElementView({model:mainElement});
App.mainRegion.show(mainElementView);
setTimeout(updateJSON, 5000);
}
// timeOut loop to check for JSON changes
var updateJSON = function(){
mainElement.fetch({url: 'http://json.json'});
App.timeOut = setTimeout(updateJSON, 5000);
}
}
}
MainElement Model:
MainElement = Backbone.Model.extend({
parse : function(response){
// parsing code
return response;
}
});
MainElementView (Layout):
MainElementView = Backbone.Marionette.Layout.extend({
template: "#main-template",
initialize:function(){
//create collection for subelements
this.subElementCollection = new SubElementCollection();
//listen to change event, and fire callback only when change in model is detected
this.model.on('change', this.render, this);
},
regions:{
subsection : ".subsection"
},
onShow: function(){
// show subelements in subsection region when mainelementview is shown on screen, but not show on every render
this.subsection.show(new SubElementCompositeView({collection:this.subElementCollection}))
},
onRender : function(){
var response = this.model.response;
// get subelements when change event fires and parse the response
this.subElementCollection.set(response,{parse:true});
}
});
SubElement Model, Collection, ItemView, CompositeView:
SubElement = Backbone.Model.extend({});
SubElementCollection = Backbone.Collection.extend({
model:SubElement,
comparator : function(model){
var price = model.get('price');
return -(price);
},
parse:function(response){
// parsing code to get data to models from differents parts of JSON
return response;
}
});
SubElementItemView = Backbone.Marionette.ItemView.extend({
template: "#subelement-template",
tagName: "tr"
});
SubElementCompositeView = Backbone.Marionette.CompositeView.extend({
template: "#subelements-template",
tagName : "table",
itemView:SubElementItemView,
itemViewContainer : "tbody",
initialize: function(){
this.collection.on('change', this.render, this);
},
appendHtml : function(collectionView,itemView,index){
// SORTING CODE
},
onRender:function(collectionView,itemView,index){
// ADD IRRELEVANT EXTERNAL STUFF TO TEMPLATE AFTER RENDER
}
});
Check out the documentation. It says:
A "change" event will be triggered if the server's state differs from the current attributes.
In your MainElementView where you bind to your model's change event: this.model.on('change', this.render, this);, you are actually saying every time your model changes, call render.
If re-drawing the whole view is too slow, due to the amount of changes. Why don't you make your rendering more fine-grained? For example you could listen for specific change events and just change the DOM elements which need changing:
this.model.on('change:Name', function () {
this.$('[name=Name]').html(this.model.get('Name'));
}, this);
It is more work to set this up, but you could make it a bit cleverer by matching the model property names to your DOM element name or something.
Backbone.js newbie here.
General question: What is the best practice to track the number of models in a collection in order to display it on the UI? My use cases can involve changes on the server side so each time the collection is sync'd I need to be able to update the UI to the correct number from storage.
I'm using Backbone.js v1.0.0 and Underscore v1.4.4 from the amdjs project and Require.js v2.1.6.
Specific example: Simple shopping cart showing "number of items in the cart" that continually updates while the user is adding/removing items. In this example I'm almost there but (1) my code is always one below the real number of models and (2) I feel that there is a much better way to do this!
Here's my newbie code.
First, I have a collection of items that the user can add to their cart with a button. (NOTE: all AMD defines and returns are removed in code examples for brevity.)
var PackagesView = Backbone.View.extend({
el: $("#page"),
events: {
"click .addToCart": "addToCart"
},
initialize: function(id) {
this.collection = new PackagesCollection([],{id: id.id});
this.collection.fetch({
reset: true
});
this.collection.on("reset", this.render, this);
},
render: function(){
//other rendering stuff here
..............
//loop through models in collection and render each one
_.each(this.collection.models, function(item){
that.renderPackages(item);
});
}
renderPackages: function(item){
var packageView = new PackageView({
model: item
});
this.$el.append(packageView.render().el);
},
Next I have the view for each individual item in the cart PackageView which is called by the PackagesView code above. I have a "add to cart" button for each Package that has a "click" event tied to it.
var PackageView = Backbone.View.extend({
tagName:"div",
template:$(packageTemplate).html(),
events: {
"click .addToCart": "addToCart"
},
render:function () {
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
},
addToCart:function(){
cartView = new CartView();
cartView.collection.create(new CartItemModel(this.model));
}
Finally, I have a CartView that has a collection of all the items in the cart. I tried adding a listenTo method to react to changes to the collection, but it didn't stay in sync with the server either.
var CartView = Backbone.View.extend({
el: $("#page"),
initialize:function(){
this.collection = new CartCollection();
this.collection.fetch({
reset: true
});
this.listenTo(this.collection, 'add', this.updateCartBanner);
this.collection.on("reset", this.render, this);
},
render: function(){
$('#cartCount').html(this.collection.length);
},
updateCartBanner: function(){
//things did not work here. Just putting this here to show something I tried.
}
End result of specific example: The .create works correctly, PUT request sent, server adds the data to the database, "reset" event is called. However, the render() function in CartView does not show the right # of models in the collection. The first time I click a "add to cart" button the $('#cartCount') element does not get populated. Then anytime after that it does get populated but I'm minus 1 from the actual count on the server. I believe this is because I have a .create and a .fetch and the .fetch is happening before the .create finishes so I'm always 1 behind the server.
End result, I'm not structuring this the right way. Any hints in the right direction would be helpful!
You can try like this:
collection.on("add remove reset sync", renderCallback)
where renderCallback is function which refresh your UI.
Found an answer to my question, but could definitely be a better method.
If I change my code so instead of a separate view for each model in the collection as I have above, I have one view that iterates over all the models and draws then it will work. I still need to call a .create followed by a .fetch with some unexpected behavior, but the end result is correct.
Note that in this code I've completely done away with the previous PackageView and everything is drawn by PackagesView now.
var PackagesView = Backbone.View.extend({
el: $("#page"),
events: {
"click .addToCart": "addToCart"
},
initialize: function(id) {
this.collection = new PackagesCollection([],{id: id.id});
this.collection.fetch({
reset: true
});
this.collection.on("reset", this.render, this);
},
render: function(){
var that = this;
var tmpl = _.template($(packageTemplate).html());
//loop through models in collection and render each one
_.each(this.collection.models, function(item){
$(that.el).append(tmpl(item.toJSON()));
});
},
addToCart:function(e){
var id= $(e.currentTarget).data("id");
var item = this.collection.get(id);
var cartCollection = new CartCollection();
var cartItem = new CartItemModel();
cartCollection.create(new CartItemModel(item), {
wait: true,
success: function() {
console.log("in success create");
console.log(cartCollection.length);
},
error:function() {
console.log("in error create");
console.log(cartCollection.length);
}
});
cartCollection.fetch({
wait: true,
success: function() {
console.log("in success fetch");
console.log(cartCollection.length);
$('#cartCount').html(cartCollection.length);
},
error:function() {
console.log("in error fetch");
console.log(cartCollection.length);
}
});
Result: The $('#cartCount') in the .fetch callback injects the correction number of models. Unexpectedly, along with the correct .html() value the Chrome console.log return is (server side had zero models in the database to start with):
in error create PackagesView.js:88
0 PackagesView.js:89
in success fetch PackagesView.js:97
1
And I'm getting a 200 response from the create, so it should be "success" for both callbacks. I would have thought that the Backbone callback syntax for create and fetch were the same. Oh well, it seems to work.
Any feedback on this method is appreciated! Probably a better way to do this.
Incidentally this goes against the general advice here, although I do have a "very simple list" so perhaps its OK in the long run.
I have created a simple Todo application on JS Fiddle to learn Backbone.JS. I have a TodosModuleView that wraps a form and TodosView which renders the Collection of Todos
window.TodosModuleView = Backbone.View.extend({
tagName: 'section',
id: 'todoModule',
events: {
'keypress #frmTodo input[type=text]': 'addTodo'
},
initialize: function() {
_.bindAll(this, 'render',
'addTodo');
this.collection.bind('reset', this.render);
this.template = _.template($('#todosModuleTmpl').html()); },
render: function() {
console.log('rendering...');
var todosView = new TodosView({ collection: this.collection });
this.$el.html(this.template({}));
this.$el.append(todosView.render().el);
return this;
},
addTodo: function(e) {
if (e.keyCode !== 13)
return;
var todo = new Todo({ title: this.$('input[name=todo]').val() });
this.collection.add(todo);
console.log('added!');
return false;
}
});
When I add a todo, I can see it added to the collection, but it does not appear to trigger render(). Also since I am using a Local Storage store, I'd expect that my newly added Todos should be persisted and rendered on next refresh, but that does not appear to happen. Looking at the Chrome's developer toolbar, I don't see anything in Local Storage
UPDATE
1st Problem solved with #mashingan's answer: use add instead of reset event. Now whats wrong with my Local Storage?
window.Todos = Backbone.Collection.extend({
model: Todo,
localStorage: new Backbone.LocalStorage('todos')
});
Could it be that variables are passed by value instead of reference as I'd expect? I have a TodosModuleView that uses TodosView to render the todo list, maybe I am doing it the wrong way?
Your LocalStorage isn't working because you're not saving anything. This:
var todo = new Todo({ title: this.$('input[name=todo]').val() });
this.collection.add(todo);
simply creates a new model and adds it to the collection, there is no hidden todo.save() call in there so the new Todo doesn't get saved. You'd have to save it yourself:
var todo = new Todo({ ... });
todo.save();
this.collection.add(todo);
You could also save everything in the collection with:
this.collection.invoke('save');
That will call save on each model in the collection. This might make sense for LocalStorage but not so much sense if you're persisting to a remote server.
If you do this:
var M = Backbone.Model.extend({});
var C = Backbone.Collection.extend({
model: M,
localStorage: new Backbone.LocalStorage('pancakes')
});
var c = new C;
c.add([
{ title: 'Fargo' },
{ title: 'Time Bandits' }
]);
Then you won't get anything in your pancakes database, but if you add c.invoke('save') at the end:
var M = Backbone.Model.extend({});
#...
c.add([ ... ]);
c.invoke('save');
You will get a couple of good movies saved.
Demo: http://jsfiddle.net/ambiguous/ZV86g/
Check backbone catalog of events: reset (collection) — when the collection's entire contents have been replaced. There is add event which should work in your case.
I'm having a look at Backbone.js, but I'm stuck. The code until now is as simple as is possible, but I seem not to get it. I use Firebug and this.moments in the render of MomentsView is an object, but all the methods from a collection don't work (ie this.moments.get(1) doesn't work).
The code:
var Moment = Backbone.Model.extend({
});
var Moments = Backbone.Collection.extend({
model: Moment,
url: 'moments',
initialize: function() {
this.fetch();
}
});
var MomentsView = Backbone.View.extend({
el: $('body'),
initialize: function() {
_.bindAll(this, 'render');
this.moments = new Moments();
},
render: function() {
_.each(this.moments, function(moment) {
console.log(moment.get('id'));
});
return this;
}
})
var momentsview = new MomentsView();
momentsview.render();
The (dummy) response from te server:
[{"id":"1","title":"this is the moment","description":"another descr","day":"12"},{"id":"2","title":"this is the mament","description":"onother dascr","day":"14"}]
The object has two models according to the DOM in Firebug, but the methods do not work. Does anybode have an idea how to get the collection to work in the view?
The problem here is that you're fetching the data asynchronously when you initialize the MomentsView view, but you're calling momentsview.render() synchronously, right away. The data you're expecting hasn't come back from the server yet, so you'll run into problems. I believe this will work if you call render in a callback to be executed once fetch() is complete.
Also, I don't think you can call _.each(this.moments) - to iterate over a collection, use this.moments.each().
Try removing the '()' when instantiate the collection.
this.moments = new Moments;
Also, as it's an asynchronous call, bind the collection's 'change' event with the rendering.
I hope it helps you.