Backbone.JS collection reset event does not appear to fire - backbone.js

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.

Related

Getting view to update on save using Backbone.js

I am learning Backbone.js and as a trial project I am creating a little WordPress user management application. So far my code shows a listing of all WordPress users and it has a form which enables you to add new users to the application.
This all works fine however when you add a new user the listing of users doesn't update automatically, you need to refresh the page to see the new user added which isn't ideal and defeats one of the benefits of Backbone.js!
I have a model for a user and then a collection which compiles all the users. I have a view which outputs the users into a ul and I have a view which renders the form. How do I make my code work so when the .save method is called the view which contains the users updates with the new user? Or is there another way to approach this?
//define the model which sets the defaults for each user
var UserModel = Backbone.Model.extend({
defaults: {
"username": "",
"first_name": "",
"last_name": "",
"email": "",
"password": "",
},
initialize: function(){
},
urlRoot: 'http://localhost/development/wp-json/wp/v2/users'
});
//define the base URL for ajax calls
var baseURL = 'http://localhost/development/wp-json/wp/v2/';
//function to define username and password
function authenticationDetails(){
var user = "myUserName";
var pass = "myPassword";
var token = btoa(user+':'+pass);
return 'Basic ' + token;
}
//add basic authorisation header to all API requests
Backbone.$.ajaxSetup({
headers: {'Authorization':authenticationDetails()}
});
//create a collection which returns the data
var UsersCollection = Backbone.Collection.extend(
{
model: UserModel,
// Url to request when fetch() is called
url: baseURL + 'users?context=edit',
parse: function(response) {
return response;
},
initialize: function(){
}
});
// Define the View
UserView = Backbone.View.extend({
model: UserModel,
initialize: function() {
// create a collection
this.collection = new UsersCollection;
// Fetch the collection and call render() method
var that = this;
this.collection.fetch({
success: function () {
that.render();
}
});
},
// Use an external template
template: _.template($('#UserTemplate').html()),
render: function() {
// Fill the html with the template and the collection
$(this.el).html(this.template({ users: this.collection.toJSON() }));
return this;
},
});
var userListing = new UserView({
// define the el where the view will render
el: $('#user-listing')
});
NewUserFormView = Backbone.View.extend({
initialize: function() {
this.render();
},
// Use an external template
template: _.template($('#NewUserTemplate').html()),
render: function() {
// Fill the html with the template and the collection
$(this.el).html(this.template());
return this;
},
events: {
'click .create-user':'addNewUser'
},
addNewUser: function(){
var newFirstName = $('.first-name').val();
var newLastName = $('.last-name').val();
var newEmail = $('.email').val();
var newPassword = $('.password').val();
var newUserName = newFirstName.toLowerCase();
var myNewUser = new UserModel({username:newUserName,first_name:newFirstName,last_name:newLastName,email:newEmail,password:newPassword});
console.log(myNewUser);
myNewUser.save({}, {
success: function (model, respose, options) {
console.log("The model has been saved to the server");
},
error: function (model, xhr, options) {
console.log("Something went wrong while saving the model");
}
});
}
});
var userForm = new NewUserFormView({
// define the el where the view will render
el: $('#new-user-form')
});
All backbone objects (models, collections, views) throw events, some of which would be relevant to what you want. Models throw change events when their .set methods are used, and Collections throw add or update events... a complete list is here.
Once you know which events are already being thrown, you can listen to them and react. For example, use listenTo - in your view's initialize, you can add:
this.listenTo(this.collection, 'add', this.render);
That will cause your view to rerender whenever a model is added to your collection. You can also cause models, collections, whatever, to throw custom events using trigger from anywhere in the code.
EDIT: For the specific case of getting your user listing view to rerender when a new user is added using the form, here are the steps you can take... In the initialize method of your UserView, after the initialize the collection, add:
this.listenTo(this.collection, 'add', this.render);
Then in your form view... assuming you want to wait until the save is complete on your server, in the addNewUser method, in the success callback of your save, add:
userlisting.collection.add(model);
This will work, since the instance of your UserView is in the global scope. Hope this one works for you!

Backbone pre rendering of collection models

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

Backbone.js - delete views

This is my second day trying to use backbone and im completely lost. I am following this tutorial - http://net.tutsplus.com/tutorials/javascript-ajax/build-a-contacts-manager-using-backbone-js-part-3/
What I have done is loaded a contacts list and rendered it to the screen, but if you look at my render1 function - this takes a form input and appends it to my template. The problem is that I can't delete these items after they are created - the others can be deleted. help please?
var ContactView = Backbone.View.extend({
tagName: "contacts",
className: "contact-container",
template: $("#contactTemplate").html(),
initialize: function(){
this.model.on('change', this.render, this);
this.model.on('add', this.render1, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .deleteUser': 'delete'
},
test: function () {
alert("here");
},
delete: function () {
this.model.destroy();
},
render: function () {
console.log(this);
var tmpl = _.template(this.template);
$(this.el).html(tmpl(this.model.toJSON()));
temp = tmpl(this.model.toJSON());
console.log(temp);
return this;
},
render1: function () {
console.log(this);
var tmpl = _.template(this.template);
temp = tmpl(this.model.toJSON());
temp='<contacts class="contact-container">'+temp+'</contacts>';
console.log(temp);
$("#contacts").append(temp);
$(this.el).html(tmpl(this.model.toJSON()));
return this;
}
});
var AddPerson = Backbone.View.extend({
el: $("#addPerson"),
// el: $("form/"),
events: {
'click': 'submit',
'submit': 'submit'
},
submit: function(e) {
// alert("here");
e.preventDefault();
this.collection = new Directory();
// var data = (contacts[0]);
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
}
});
seasick, there are quite a few issues in this code.
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
Contact is a Backbone.Model but ContactView is a Backbone.View. Yet, you are adding both to the this.collection (which I assume is a Backbone.Collection of Contact?). See the problem here? In Backbone, there is no such concept of a 'collection of views'. You just get one concept: views, that are tied to a model.
So, here, you create a Contact and you add it to the Collection. That is all! It takes care of the Model part. The rendering part needs to be handled with events and renders.
When you add a model to a collection (this.collection.add(contact)), the collection will trigger a 'add' event, that you can hook to with a .on to create a new ContactView and append it to the DOM somewhere.
So when you write this...
this.model.on('add', this.render1, this);
You are actually saying 'When the Contact model triggers an add event, run render1', which isn't what you want, what you probably want is a collection.on('add', ...). The model will never trigger an add event (well, you could make it trigger one, but it wouldn't be an expected behavior!), the add/remove events are at the collection level.
In other words, you are missing some binding on the collection in your AddPerson view to 'react' to adding a new Contact to the collection. The code of the function bound to the add event should probably look a bit like:
onAdd: function(newContact){
var newContactView = new ContactView({model: newContact});
$("#contacts").append(newContactView.render().el);
}
There are other issues in your code, but I guess an outline of the steps to take would be like:
Remove the binding to add in ContactView: ContactView is only concerned with one contact, not how to manage multiple contacts. This is probably why you are having issues with only some (the first one?) of the contacts 'working'
Move that logic to the AddContact view which seems to be more concerned with the Collection of contacts. Use the collection 'add' event to create new ContactView and append them to the DOM
Hope this helps!

Backbone append models works in console but not after $(document).ready

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}});

How can I bind the model to the view?

When the view is initialized, how can I bind the model to the specific View that is created? The view is current initialized at the start of the application. Also, how can I bind the model to the collection?
(function ($) { //loads at the dom everything
//Creation, Edit, Deletion, Date
var Note = Backbone.Model.extend({
defaults: {
text: "write here...",
done: false
},
initialize: function (){
if(!this.get("text")){
this.set({"text": this.default.text});
}
},
edit: function (){
this.save({done: !this.get("done")});
},
clear: function (){
this.destroy();
}
});
var NoteList = Backbone.Collection.extend({
model:Note
});
var NoteView = Backbone.View.extend ({
el: "body",
initialize: function(){
alert("initialized");
var list = new NoteList;
return list;
},
events: {
"click #lol" : "createNote"
},
createNote : function(){
var note = new Note;
this.push(note);
alert("noted");
}
});
var ninja = new NoteView;
})(jQuery);
Update
I just took a look at #James Woodruff's answer, and that prompted me to take another look at your code. I didn't look closely enough the first time, but I'm still not sure what you're asking. If you're asking how to have a model or view listen for and handle events triggered on the other, then check out James's example of calling bind() to have the view listen for change (or change:attr) events on the model (although I'd recommend using on() instead of bind(), depending what version of Backbone you're using).
But based on looking at your code again, I've revised my answer, because I see some things you're trying to do in ways that don't make sense, so maybe that's what you're asking about.
New Answer
Here's the code from your question, with comments added by me:
var NoteView = Backbone.View.extend ({
// JMM: This doesn't make sense. You wouldn't normally pass `el`
// to extend(). I think what you really mean here is
// passing el : $( "body" )[0] to your constructor when you
// instantiate the view, as there can only be one BODY element.
el: "body",
initialize: function(){
alert("initialized");
// JMM: the next 2 lines of code won't accomplish anything.
// Your NoteList object will just disappear into thin air.
// Probably what you want is one of the following:
// this.collection = new NoteList;
// this.list = new NoteList;
// this.options.list = new NoteList;
var list = new NoteList;
// Returning something from initialize() won't normally
// have any effect.
return list;
},
events: {
"click #lol" : "createNote"
},
createNote : function(){
var note = new Note;
// JMM: the way you have your code setup, `this` will be
// your view object when createNote() is called. Depending
// what variable you store the NoteList object in (see above),
// you want something here like:
// this.collection.push( note ).
this.push(note);
alert("noted");
}
});
Here is a revised version of your code incorporating changes to the things I commented on:
var NoteView = Backbone.View.extend( {
initialize : function () {
this.collection = new NoteList;
},
// initialize
events : {
"click #lol" : "createNote"
},
// events
createNote : function () {
this.collection.push( new Note );
// Or, because you've set the `model` property of your
// collection class, you can just pass in attrs.
this.collection.push( {} );
}
// createNote
} );
var note = new NoteView( { el : $( "body" )[0] } );
You have to bind views to models so when a model updates [triggers an event], all of the corresponding views that are bound to the model update as well. A collection is a container for like models... for example: Comments Collection holds models of type Comment.
In order to bind a view to a model they both have to be instantiated. Example:
var Note = Backbone.Model.extend({
defaults: {
text: "write here..."
},
initialize: function(){
},
// More code here...
});
var NoteView = Backbone.View.extend({
initialize: function(){
// Listen for a change in the model's text attribute
// and render the change in the DOM.
this.model.bind("change:text", this.render, this);
},
render: function(){
// Render the note in the DOM
// This is called anytime a 'Change' event
// from the model is fired.
return this;
},
// More code here...
});
Now comes the Collection.
var NoteList = Backbone.Collection.extend({
model: Note,
// More code here...
});
Now it is time to instantiate everything.
var Collection_NoteList = new NoteList();
var Model_Note = new Note();
var View_Note = new NoteView({el: $("Some Element"), model: Model_Note});
// Now add the model to the collection
Collection_NoteList.add(Model_Note);
I hope this answers your question(s) and or leads you in the right direction.

Resources