Jump To …

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 Collection

backed 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 Collection

backed by LocalStorage
Note: this is the entire phone number database, for all contacts...

PhoneNumbers = Backbone.Collection.extend(

Our collection of model

  model: PhoneNumber

local storage adapter

  localStorage: new Backbone.LocalStorage("com.thomporter.contactsDemo.phone_numbers"),
)

PhoneInListView

View 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'))
    @
)

PhoneListView

view 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)

    )
    @
)

ContactInListView

View 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)


)

ContactsView

View 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"

    @
)

ContactView

individual 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()

)

ContactApp

Our 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 Method

renders 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 Method

initialized 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()