Binding embedded data

One of the most common issues with Backbone is how to deal with complex model data:

{
"name": "John Doe",
"address": {
"street": "Seleme",
"number": "1975 int 6",
"city": "Culiacán"
  },
"phones": [{
"label": "Home",
"number": "55 555 123"
  }, {
"label": "Office",
"number": "55 555 234"
  }],
"emails": [{
"label": "Work",
"email": "[email protected]"
  }]
}

It could be easy to render a read-only view for this model data; however, the real challenge is how to bind form actions with embedded arrays. In Backbone, it is difficult to use the event system on array objects; if you push a new item in the list, no event will be triggered. This makes it difficult to keep model data in sync with the a view that edits its contents.

Binding an embedded list

Imagine that our Contacts App will now allow us to add more than one phone and email. We will need to change the edit form view to add support for adding, removing, and modifying items on the array of phones and emails:

Binding an embedded list

Figure 3.3. Contact form layout with phone and email lists

Figure 3.3 shows the result of adding a New button to allow the user to dynamically add the number of phones and emails he/she wants. Each item in the list should include a Delete button too to allow the user to remove them.

To render the phone and email lists and sync the forms with the model, we will follow a different strategy; Figure 3.4 illustrates how our strategy will look:

Binding an embedded list

Figure 3.4 Embedded array rendering strategy

We will create two new Backbone collections, one for phones and another for emails. With the data in the Contact model we can initialize these collections and render them as regular CollectionView.

As we saw in the previous chapter, CollectionView objects take care of the changes in the collection that it renders, so that we can modify the collection object and the view will behave as expected.

When the user clicks on the Save button, we can serialize the content of these collections and update the model before we call the save() method.

Each item for phones and emails will have a very similar template:

<script id="contact-form-phone-item" type="text/template">
<div class="col-sm-4 col-md-2">
<input type="text" class="form-control description" 
placeholder="home, office, mobile"
 value="<%= description %>" />
</div>
<div class="col-sm-6 col-md-8">
<input type="text" class="form-control phone" 
placeholder="(123) 456 7890" value="<%= phone %>" />
</div>
<div class="col-sm-2 col-md-2 action-links">
<a href="#" class="pull-rigth delete">delete</a>
</div>
</script>

This template will be used as a ModelView for a CollectionView:

// apps/contacts/contactEditor.js
class PhoneListItemView extends ModelView {
  constructor(options) {
    super(options);
    this.template = '#contact-form-phone-item';
  }

  get className() {
    return 'form-group';
  }
}

class PhoneListView extends CollectionView {
  constructor(options) {
    super(options);
    this.modelView = PhoneListItemView;
  }
}

The contact form now should include two regions for PhoneListView and EmailListView:

<div class="panel panel-simple">
<div class="panel-heading">
    Phones
<button id="new-phone"
class="btn btn-primary btn-sm pull-right">New</button>
</div>
<div class="panel-body">
<form class="form-horizontal phone-list-container"></form>
</div>
</div>

<div class="panel panel-simple">
<div class="panel-heading">
    Emails
<button id="new-email"
 class="btn btn-primary btn-sm pull-right">New</button>
</div>
<div class="panel-body">
<form class="form-horizontal email-list-container"></form>
</div>
</div>

ContactForm should be changed to support regions; we will extend from Layout instead of ModelView:

// apps/contacts/contactEditor.js
class ContactForm extends Layout {
  constructor(options) {
    super(options);
    this.template = '#contact-form';
    this.regions = {
      phones: '.phone-list-container',
      emails: '.email-list-container'
    };
  }

  // ...
}

We will need two new models: Phone and Email. Because both models are very similar, I will only show Phone:

// apps/contacts/models/phone.js
'use strict';

App.Models = App.Models || {};

class Phone extends Backbone.Model {
  get defaults() {
    return {
      description: '',
      phone: ''
    };
  }
}

App.Models.Phone = Phone;

And collection that uses the Phone model:

// apps/contacts/collections/phoneCollection.js
'use strict';

App.Collections = App.Collections || {};

class PhoneCollection extends Backbone.Collection {
  constructor(options) {
    super(options);
  }

  get model() {
    return App.Models.Phone;
  }
}

App.Collections.PhoneCollection = PhoneCollection;

Now that we have the necessary objects to render the form, let's put them all together in the controller. First, we need to create the collection instances from the model data:

// apps/contacts/contactEditor.js
class ContactEditor {
  // ...

  showEditor(contact) {
// Data
    var phonesData = contact.get('phones') || [];
    var emailsData = contact.get('emails') || [];
this.phones = new App.Collections.PhoneCollection(phonesData);
this.emails = new App.Collections.EmailCollection(emailsData);

    // ...
  }

  // ...
}

With the collections in place, we can build CollectionViews properly:

// apps/contacts/contactEditor.js
class ContactEditor {
  // ...

  showEditor(contact) {
    // ...

    // Create the views
    var layout = new ContactFormLayout({model: contact});
    var phonesView = new PhoneListView({collection: this.phones});
    var emailsView = new EmailListView({collection: this.emails});
    var contactForm = new ContactForm({model: contact});
    var contactPreview = new ContactPreview({model: contact});

    // ...
  }

  // ...
}

The phonesView and emailsView can be rendered in the regions exposed in the contactForm object:

// apps/contacts/contactEditor.js
class ContactEditor {
  // ...

  showEditor(contact) {
    // ...

    // Render the views
    this.region.show(layout);
    layout.getRegion('form').show(contactForm);
    layout.getRegion('preview').show(contactPreview);
    contactForm.getRegion('phones').show(phonesView);
    contactForm.getRegion('emails').show(emailsView);

    // ...
  }

  // ...
}

When the user clicks on the New button, a new item in a proper list should be added:

// apps/contacts/contactEditor.js
class ContactForm extends Layout {
  // ...

  get events() {
    return {
      'click #new-phone': 'addPhone',
      'click #new-email': 'addEmail',
      'click #save': 'saveContact',
      'click #cancel': 'cancel'
    };
  }

  addPhone() {
    this.trigger('phone:add');
  }

  addEmail() {
    this.trigger('email:add');
  }

  // ...
}

The ContactForm knows nothing about the collections that we are using in the controller, so they can't add an item in the collection directly; the controller should listen for events in the contactForm and update the collection:

// apps/contacts/contactEditor.js
class ContactEditor {
  // ...

  showEditor(contact) {
    // ...

    this.listenTo(contactForm, 'phone:add', this.addPhone);
    this.listenTo(contactForm, 'email:add', this.addEmail);


    // ...
  }

  addPhone() {
    this.phones.add({});
  }

  addEmail() {
    this.emals.add({});
  }

  // ...
}

When the user clicks on the delete link in an item of the list, the item should be removed from the collection:

// apps/contacts/contactEditor.js
class PhoneListItemView extends ModelView {
  //...

  get events() {
    return {
      'click a': 'deletePhone'
    };
  }

  deletePhone(event) {
    event.preventDefault();
    this.trigger('phone:deleted', this.model);
  }
}

As we did with add, the controller will take care of managing the collection data by attaching an event listener in the list view:

// apps/contacts/contactEditor.js
class ContactEditor {
  // ...

  showEditor(contact) {
    // ...

    this.listenTo(phonesView, 'item:phone:deleted',
(view, phone) => {
this.deletePhone(phone);
}
);
    this.listenTo(emailsView, 'item:email:deleted',
 (view, email) => {
this.deleteEmail(email);
}
);

    // ...
  }

  deletePhone(phone) {
    this.phones.remove(phone);
  }

  deleteEmail(email) {
    this.emails.remove(email);
  }

  // ...
}

As you can see in the preceding snippets, adding items to the list (and removing them) is pretty simple; we just need to update the underlying collection and the views will be updated automatically. We did a great job with CollectionViews in the previous chapter.

To save the phone and email attributes in the model, we need to extract the data stored in the collections and replace the existing data in the model:

// apps/contacts/contactEditor.js
class ContactEditor {
  // ...

  saveContact(contact) {
    var phonesData = this.phones.toJSON();
    var emailsData = this.emails.toJSON();

    contact.set({
      phones: phonesData,
      emails: emailsData
    });

    contact.save(null, {
      success() {
        // Redirect user to contact list after save
        App.notifySuccess('Contact saved');
        App.router.navigate('contacts', true);
      },
      error() {
        // Show error message if something goes wrong
        App.notifyError('Something goes wrong');
      }
    });
  }
  // ...
}

However, the collections are not in sync with the forms and you will end up with empty emails and phones. To fix this, we need to bind the models with the inputs:

// apps/contacts/contactEditor.js
class PhoneListItemView extends ModelView {
  // ...

  get events() {
    return {
      'change .description': 'updateDescription',
      'change .phone': 'updatePhone',
      'click a': 'deletePhone'
    };
  }

  updateDescription() {
    var $el = this.$('.description');
    this.model.set('description', $el.val());
  }

  updatePhone() {
    var $el = this.$('.phone');
    this.model.set('phone', $el.val());
  }

  // ...
}

Now, if you click the Save button, the data about the phones and emails will be stored as expected.

This way of binding embedded arrays in views through intermediate collections simplifies the way you work with lists and will make your code a lot simpler and more maintainable.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.59.227.82