I'm experimenting with Backbone.js by creating a table view, and a seperate row view and trying to add the row to the table:
I have:
- A Contact model
- A Contacts collection
- A Contacts view (acts as the
main view)
- A ContactRow view
So far, the project is working fine - except for a button that is supposed to trigger the function that adds the row.
Here is my code so far:
$(function($) {
window.Contact = Backbone.Model.extend({
defaults: {
first_name: "John",
last_name: "Smith",
address: "123 Main St"
}
});
window.Contacts = Backbone.Collection.extend({
model: Contact
});
window.ContactRow = Backbone.View.extend({
//el: $("#contacts-table table tbody"),
row_template: _.template($("#contact-row").html()),
initialize: function() {
_.bindAll(this, "render")
},
render: function() {
$("tbody").html("<tr><td>Look at me, I'm a row!</td></tr>");
return this;
}
});
window.ContactsView = Backbone.View.extend({
el: $("#contacts-container"),
events: {
"click button#add_contact": "addContact"
},
template: _.template($("#contacts-table").html()),
initialize: function() {
_.bindAll(this, "render", "addContact", "appendContact");
this.collection = new Contacts();
this.collection.bind("add", this.appendContact);
var contactRow = new ContactRow();
this.render();
this.appendContact(); // Just a test to see if the function is working
},
render: function() {
$("#button-container").append("<button id='add_contact'>Add Contact</button>");
$(this.el).html(this.template);
_(this.collection.models).each(function(contact) {
appendContact(contact);
}, this)
},
addContact: function() {
console.log("yup, it works!"); // well, not yet
var contact = new Contact();
this.collection.add(contact);
},
appendContact: function(contact) {
var contactRow = new ContactRow({
model: contact
});
$("body").append(contactRow.render().el);
}
});
var contactsView = new ContactsView();
}, jQuery);
As you can see, I have an addContact function that is tied to the click event of the "Add Contact" button that is being appended to a div element on the main page during the render process.
I'm attempting to write log messages to the console, but the button doesn't seem to be firing off the method and I can't figure out why.
It's the end of the day and my brain is fried so I'd appreciate any pointers on this. Thanks.
Here's a working example. I updated the code with best practices for using Backbone.
Notice I didn't add the button through a Backbone view. The button is part of the html body, and I just subscribe to its click event and then add a contact to the contacts collection.
<html>
<head>
<script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js'></script>
<script type='text/javascript' src='http://ajax.cdnjs.com/ajax/libs/underscore.js/1.1.4/underscore-min.js'></script>
<script type='text/javascript' src='http://cdnjs.cloudflare.com/ajax/libs/backbone.js/0.5.3/backbone-min.js'></script>
<script type='text/javascript'>
$(function() {
//initialize the contacts collection and add some contacts
contacts = new Contacts();
contacts.add(new Contact());
contacts.add(new Contact());
//only need to render the ContactsView once
var view = new ContactsView({ collection: contacts });
$("body").append(view.render().el);
//adding a contact to the contacts list when the
//button is clicked
$("#add-contact").click(function() {
contacts.add(new Contact());
});
});
Contact = Backbone.Model.extend({
defaults: {
first_name: "John",
last_name: "Smith",
address: "123 Main St"
}
});
Contacts = Backbone.Collection.extend({
model: Contact
});
ContactRow = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
this.template = _.template($("#contact-row").html());
},
//every backbone view has a tagName. the default tagName is 'div'
//we're changing it to a table row
tagName: 'tr',
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
ContactsView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, "render");
this.headerTemplate = $("#contacts-table-header").html();
this.collection.bind("add", this.renderContact, this);
},
//the ContactsView element will be a table
tagName: 'table',
render: function() {
$(this.el).html(this.headerTemplate);
this.collection.each(function(contact) {
this.renderContact(contact);
}, this);
return this;
},
renderContact: function(contact) {
var contactView = new ContactRow({ model: contact });
$(this.el).append(contactView.render().el);
}
});
</script>
<script type='text/template' id='contact-row'>
<td><%= first_name %></td>
<td><%= last_name %></td>
<td><%= address %></td>
</script>
<script type='text/template' id='contacts-table-header'>
<thead>
<th>First Name</th>
<th>Last Name</th>
<th>Address</th>
</thead>
</script>
</head>
<body>
<button id="add-contact">Add Contact</button>
</body>
</html>
This is because you're appending<button id='contact'>
after backbone has traversed your event collection.
When you create a backbone view delegateEvents is called behind the scenes. This is where backbone looks at your events hash and wires everything up. To fix Either:
So your render function may look like:
render: function() {
$("#button-container").append("<button id='add_contact'>Add Contact</button>");
$(this.el).html(this.template);
this.delegateEvents(); // re-wire events matching selectors in the event hash
_(this.collection.models).each(function(contact) {
appendContact(contact);
}, this)
return this; // you should also do this so you can chain
},
Update:
It seems odd that you'd have to manually call delegateEvents
. It could also be that #button-contianer
isn't a child of the view's el
. All of the selectors in that event hash are scoped to el
, so if #button-contianer
isn't a child of it the button#add_contact
selector will never find anything. As a proof of concept, try this: in your render method:
render: function() {
console.log($(this.el).find("button#add_contact").length) // get anything?
...
_(this.collection.models).each(function(contact) {
appendContact(contact);
}, this)
This code won't work, because you don't have a variable named appendContact
. Should be:
_(this.collection.models).each(this.appendContact(contact), this);