contacts.coffee | |
---|---|
copyright 2012 Thomas Porter - www.thomporter.com The following demo illustrates the usage of pure Backbone.js to create a "Contacts" application. It allows users to create contacts, edit & save them. Each contact can have multiple phone numbers. Contacts (and their associated phone numbers) are saved to LocalStorage using the Backbone.LocalStorage plugin. It is by no means a "complete" contacts app, and I don't suggest using it for anything more than a tool to learn Backbone. I welcome insights from other programmers: http://thomporter.com/contact My next little project will be to re-write this entire app using Marrionette.js. | |
Contact Model | Contact = Backbone.Model.extend(
defaults:
first_name: ''
last_name: ''
email: ''
dob: ''
notes: ''
) |
PhoneNumber Model | PhoneNumber = Backbone.Model.extend(
defaults:
contact_id: 0
type: ''
number: ''
) |
Contacts Collectionbacked by LocalStorage | Contacts = Backbone.Collection.extend( |
This is a collection of "Contact" | model: Contact |
Local storage adapter | localStorage: new Backbone.LocalStorage("com.thomporter.contactsDemo.contacts") |
Initialization | initialize: -> |
set our sorting function | @comparator = @comparatorDefault |
sort the collection | @sort() |
Default comparator sorts by last name, then frist | comparatorDefault: (model) ->
return model.get('last_name') + ' ' + model.get('first_name'); |
FirstThenLast comparator for sorting by first name, then last | comparatorFirstThenLast: (model) ->
return model.get('first_name') + ' ' + model.get('last_name');
) |
PhoneNumbers Collectionbacked by LocalStorage | PhoneNumbers = Backbone.Collection.extend( |
Our collection of model | model: PhoneNumber |
local storage adapter | localStorage: new Backbone.LocalStorage("com.thomporter.contactsDemo.phone_numbers"),
) |
PhoneInListViewView for each number in the phone list for a contact. | PhoneInListView = Backbone.View.extend( |
HTML tag name used for this view | tagName: 'li' |
Class name for that tag | className: 'phoneInListView' |
Initialization method | initialize: (options) -> |
Store our model | @model = options.model |
Store a pointer to the app | @app = options.app |
Define events to bind to | events:
'click': 'clickMe' |
Fired when a user clicks on a this view | clickMe: (e) -> |
Prevent any other actions | e.preventDefault() |
Call the app's phoneNumberClick method and pass this view's model | @app.phoneNumberClick(@model) |
Render method | render: -> |
Create the HTML for a single phone number and inject it into the view's element. | $(@el).html('<span>' + @model.get('type') + ':</span> ' + @model.get('phone_number'))
@
) |
PhoneListViewview for the list of phone numbers in the contact view | PhoneListView = Backbone.View.extend( |
HTML tag name used for this view | tagName: 'ul' |
Class name used for that tag | className: 'phoneList' |
Initialize Method | initialize: (options) -> |
save a pointer to the app | @app = options.app |
save the collection for this view | @collection = options.collection |
Render Method | render: -> |
Iterate over each of the models in the collection | @collection.each( (p) => |
If the contact ID of this phone number matches the contact we're editing it... | if p.get('contact_id') == @app.editingContact |
... create & render it. | v = new PhoneInListView({model:p, app:@app})
$(v.render().el).appendTo(@el)
)
@
) |
ContactInListViewView for a contact in the list view | ContactInListView = Backbone.View.extend( |
HTML tag name for this view | tagName: 'div' |
Class name for that tag | className: 'contactInListView' |
Initialization Method | initialize: (options) -> |
Save our model | @model = options.model |
Save a pointer to the app | @app = options.app |
Define events to bind to | events:
'click' : 'clickMe' |
Render Method | render: -> |
Put the names together and inject them into the view | $(@el).html(@model.get('last_name') + ', ' + @model.get('first_name')) |
Return this | @ |
Fired when a user clicks on a contact in the list | clickMe: (e) -> |
Remove the contact-on class from the currently selected contact (if any) | $('.contact-on').removeClass('contact-on') |
Add the contact-on class to this view | $(@el).addClass('contact-on') |
Show the contact's form (via the App's method.) | @app.showContactForm(@model)
) |
ContactsViewView to hold our list of contacts | ContactsView = Backbone.View.extend( |
Initialize Method | initialize: (options) -> |
store a pointer to our view element | @el = $('#contactList') |
store a pointer to our collection | @collection = options.collection |
store a pointer to our app | @app = options.app |
Render Method | render: -> |
remove any currently listed contacts | @el.find('.contactInListView').remove() |
iterate over the collection of contacts | @collection.each( (contact) => |
create a view for this contact in the list | v = new ContactInListView({model:contact, app: @app}) |
render & append the view to our element | $(v.render().el).appendTo(@el)
) |
return "this" | @
) |
ContactViewindividual view for a contact when selected | ContactView = Backbone.View.extend( |
Initialize Method | initialize: (options) -> |
store a pointer to our model | @model = options.model |
store a pointer to the app | @app = options.app |
define the events on this view | events:
'click .btnSaveContact': 'btnSaveClick'
'click .btnCancelContact': 'btnCancelClick'
'click .addPhone': 'addPhoneClick'
'click .savePhone': 'savePhoneClick' |
Render Method | render: -> |
render the model using the template and update our elements HTML | $(@el).html(_.template($('#tmplContactView').html(), @model.attributes)) |
return "this" | @ |
btnSaveClick method | btnSaveClick: (e) -> |
prevent default action | e.preventDefault() |
call the app's saveContactClick method and pass our model | @app.saveContactClick(@model) |
btnCancelClick method | btnCancelClick: (e) -> |
prevent default action | e.preventDefault() |
call the app's cancelContactClick method and pass our model | @app.cancelContactClick(@model)
addPhoneClick: (e) -> |
prevent default action | e.preventDefault() |
call the app's addPhoneClick method | @app.addPhoneClick()
savePhoneClick: (e) -> |
prevent default action | e.preventDefault() |
call the app's savePhoneClick method | @app.savePhoneClick()
) |
ContactAppOur Main Application View | ContactApp = Backbone.View.extend( |
bind to the contactsUI div | |
place to hold the currently displayed contact | editingContact: 0 |
place to hold the phone number currently being edited | editingPhone: 0
initialize: -> |
create our contacts collection | @contacts = new Contacts(null, @) |
render the phone numbers whenever the collection changes or resets | @contacts.bind("reset", @renderContacts, @)
@contacts.bind("change", @renderContacts, @) |
becase we sort, we just redraw the whole list... | @contacts.bind("add", @renderContacts, @) |
create our phoneNumbers collection | @phoneNumbers = new PhoneNumbers(null, @) |
render the phone numbers whenever the collection changes or resets | @phoneNumbers.bind("reset", @renderPhoneNumbers, @)
@phoneNumbers.bind("change", @renderPhoneNumbers, @) |
no sorting here, we should really just appendOne | @phoneNumbers.bind("add", @renderPhoneNumbers, @) |
return "this" | @ |
bind to events | events:
'click a.loadDB': 'initDB'
'click a.clearDB': 'clearDB'
'click a.addContact': 'addContactClick' |
renderContacts Method | renderContacts: -> |
create the contacts list view | @contactsListView = new ContactsView({collection: @contacts, app: @}) |
render the list view | @contactsListView.render() |
showContactForm Method | showContactForm: (model) -> |
store the id of the contact we're going to edit | @editingContact = model.get('id') |
create the view for the contact | @contactView = new ContactView({model:model, app: @}) |
inject the HTML of our new view into our editing area | $('#contactView').html(@contactView.render().el) |
render any phone numbers for this contact | @renderPhoneNumbers() |
saveContactClick Method(called from ContactView) | saveContactClick: (model) -> |
iterate over each of the form fields as an array | $.each($('#contactForm').serializeArray(), -> |
update the mode's and | model.set(this.name,this.value)
) |
if we're editing a contact, save it | if (@editingContact)
@contacts.get(model.get('id')).save() |
otherwise create a new one and then show the new contact's form. | else
@contacts.create(model)
@showContactForm(model) |
renderPhoneNumbers Methodrenders the list of phone numbers assigned to the currently active contact | renderPhoneNumbers: -> |
create a new PhoneListView with our collection of phone numbers | @phoneListView = new PhoneListView({collection: @phoneNumbers, app: @}) |
update the HTML of the phone list | $('#phoneList').html(@phoneListView.render().el) |
return "this" | @ |
cancelContactClick Method(called from ContactView) this method could be removed, opting instead to directly call showContactForm from the view, but in reality an "are you sure" prompt might be a good idea. =) | cancelContactClick: (model) -> |
call our showContactForm Method passing the model | @showContactForm(model) |
return "this" | @ |
addPhoneClick Method(called from ContactView) | addPhoneClick: -> |
show the phone form and clear the inputs | $('#phoneForm').show().find('input').val('') |
set the editing phone number to 0 so when they save, it creates a new record | @editingPhone = 0 |
return "this" | @ |
savePhoneClick Method(called from ContactView) | savePhoneClick: -> |
get the label & phone number values | l = $('#phone_label').val()
p = $('#phone_number').val() |
bail if either are blank... | return if (l == '' || p == '') |
create an object with our data | data =
contact_id: @editingContact
type: l
phone_number: p |
save the current record if we're editing a phone number | if (@editingPhone)
@phoneNumbers.get(@editingPhone).set(data).save() |
otherwise create a new one | else
@phoneNumbers.create(data) |
hide the phone number form. | $('#phoneForm').hide() |
return "this" | @ |
phoneNumberClick Method(called from PhoneListView) | phoneNumberClick: (model) -> |
store the id of the phone number clicked | @editingPhone = model.get('id') |
update the editor label & phone number inputs & show them. | $('#phone_label').val(model.get('type'))
$('#phone_number').val(model.get('phone_number'))
$('#phoneForm').show() |
addContactClick Method(called from ContactsView) | addContactClick: (e) -> |
call our showContactForm method passing a new Contact object | @showContactForm(new Contact) |
clearDB Method | clearDB: -> |
There must be a better way......... I had to add the outer loop because "destroy" would mess up the array indexing, and .each would skip records. I tried doing a for loop with i = @contacts.models.length, but that didn't work either!? this out loop was all that would do it for me. =( | while (@contacts.models.length)
@contacts.each((contact)->
contact.destroy()
)
$('.contactInListView').remove() |
return "this" | @ |
intDB Methodinitialized the databae (in this case localstorage) with some sample data | initDB: -> |
array of data | data = [
{first_name: 'Babe', last_name: 'Ruth', email: '714@xyz.com'},
{first_name: 'Maralyn', last_name: 'Monroe', email: 'normajean@xyz.com'},
{first_name: 'Albert', last_name: 'Einstien', email: 'emc2@xyz.com'},
{first_name: 'Amadeaus', last_name: 'Motzart', email: 'trebble@xyz.com'},
{first_name: 'Harry', last_name: 'Potter', email: 'underthestairs@xyz.com'},
{first_name: 'Henry D.', last_name: 'Theraeu', email: 'inthewoods@xyz.com'},
{first_name: 'George', last_name: 'Jones', email: 'whitelightning@xyz.com'},
{first_name: 'Benjamin', last_name: 'Franklin', email: 'electric@xyz.com'},
{first_name: 'Dinocrates', last_name: 'Rhodes', email: 'alexandria@xyz.com'},
{first_name: 'Elvis', last_name: 'Presley', email: 'leftthebuilding@xyz.com'},
{first_name: 'Robert', last_name: 'Oppenheimer', email: 'whathaveidone@xyz.com'},
{first_name: 'Nikola', last_name: 'Tesla', email: 'thecoil@xyz.com'},
{first_name: 'Abraham', last_name: 'Lincoln', email: 'emancipate@xyz.com'},
{first_name: 'Mark', last_name: 'Antony', email: 'cleopatra@xyz.com'},
] |
Iterate over our data array (binding "this" to our app!) | $.each(data, (index,contact) => |
create the new contact | c = @contacts.create(contact) |
create a fake phone number for the contact. | @phoneNumbers.create(
contact_id: c.id
type: 'Home'
phone_number: '888-888-8888'
)
) |
return "this" | @
) |
I LOVE COFFEESCRIPT!!! | $ -> |
this modifies underscore's template library to use moustache style variables and such | _.templateSettings =
evaluate: /\{\{#([\s\S]+?)\}\}/g, # {{# console.log("blah") }}
interpolate: /\{\{[^#\{]([\s\S]+?)[^\}]\}\}/g, # {{ title }}
escape: /\{\{\{([\s\S]+?)\}\}\}/g, # {{{ title }}} |
create the app and store it in the jQuery object. | $.c = new ContactApp({el:$('#contactsUI')}); |
fecth our contacts & phone numbers from local storage | $.c.contacts.fetch()
$.c.phoneNumbers.fetch()
|