Uploading images from Backbone

To allow us to upload files from our Backbone application, we should create an input file to be able to show a Choose file dialog. This could be done in the ContactEditor sub-application by changing the ContactPreview class to add this functionality. So let's change the current template and add the input:

<div class="box thumbnail">
<div class="photo">
<% if (avatar && avatar.url) { %>
<imgsrc="<%= avatar.url %>" alt="Contact photo" />
<% } else { %>
<imgsrc="http://placehold.it/250x250" alt="Contact photo" />
<% } %>
<input id="avatar" name="avatar" type="file" 
style="display: none" />
</div>
<!-- ... -->
</div>

Note that we have created a hidden input file field; we don't want to show the input field, but we want the control to open a Select File dialog. As the input is hidden, when the user clicks on the current image, we will show the file chooser:

// apps/contacts/views/contactPreview.js
class ContactPreview extends ModelView {
// ...

  get events() {
    return {
      'click img': 'showSelectFileDialog'
    };
  }

showSelectFileDialog() {
    $('#avatar').trigger('click');
  }

  // ...
}

When the user clicks on the image, it triggers a click event on the input; this will open the Open file dialog and allow the user to select a file from his/her hard drive. After the user selects the file, the browser triggers a change event on the file input that we can use to process the selection:

// apps/contacts/views/contactPreview.js
class ContactPreview extends ModelView {
// ...

  get events() {
    return {
      'click img': 'showSelectFileDialog',
'change #avatar': 'fileSelected'
    };
  }

  // ...
}

The change event will call the fileSelected()method that is responsible for processing the selected file. As we have seen in Chapter 1, Architecture of a Backbone application views should not talk to the server directly; for this reason, the view should not make any AJAX calls.

The best place to upload the image is in the Contact model, so the view should only get the selected file and delegate this process to the controller:

// apps/contacts/views/contactPreview.js
class ContactPreview extends ModelView {
  // ...

fileSelected(event) {
event.preventDefault();

var $img = this.$('img');

    // Get a blob instance of the file selected
var $fileInput = this.$('#avatar')[0];
varfileBlob = $fileInput.files[0];

    // Render the image selected in the img tag
varfileReader = new FileReader();
fileReader.onload = event => {
      $img.attr('src', event.target.result);

      // Set the avatar attribute only if the
      // model is new
      if (this.model.isNew()) {
this.model.set({
          avatar: {
url: event.target.result
          }
        });
      }
    };
fileReader.readAsDataURL(fileBlob);

this.trigger('avatar:selected', fileBlob);
  }
}

When a file is selected, we create a blob object and trigger an event with the object attached to be processed by the controller. Note that we use the HTML 5 API to immediately show the selected image as the avatar preview:

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

showEditor(contact) {
    // ...

this.listenTo(contactPreview, 'avatar:selected', blob => {
this.uploadAvatar(contact, blob);
    });
  }
}

The uploadAvatar() method takes a file blob as argument and delegates the server connection to the Contact model:

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

uploadAvatar(contact, blob) {
    // Tell to others that upload will start
this.trigger('avatar:uploading:start');

contact.uploadAvatar(blob, {
      progress: (length, uploaded, percent) => {
        // Tell to others that upload is in progress
this.trigger('avatar:uploading:progress',
                     length, uploaded, percent);
      },
      success: () => {
        // Tell to others that upload was done successfully
this.trigger('avatar:uploading:done');
      },
      error: err => {
        // Tell to others that upload was error
this.trigger('avatar:uploading:error', err);
      }
    });
  }
}

The controller will trigger 'avatar:uploading:*' events to reflect the status of the uploading process. These events can be listened for the view to give visual feedback to the user. Figure 5.3 graphically shows the communication between the controller and the view:

Uploading images from Backbone

Figure 5.3 Event communication between the view and controller

The uploadEvent() method in the Contact model accepts a blob object as the first argument, which is the file that will be uploaded, and an options object with three possible functions that will be called as the communication with the server proceeds.

As you may guess, success and error callbacks will be called if the server accepts the file or if a error happens, respectively. Large files are divided and uploaded to the server in chunks; the progress()callback is called as the chunks are received in the server. With the information provided in the progress() handler, we can update a progress bar to show the progress to the user:

// apps/contacts/views/contactPreview.js
class ContactPreview extends ModelView {
  constructor(options) {
    super(options);
this.template = template;

this.model.on('change', this.render, this);

    if (options.controller) {
this.listenTo(
options.controller, 'avatar:uploading:start',
this.uploadingAvatarStart, this
      );
this.listenTo(
options.controller, 'avatar:uploading:done',
this.uploadingAvatarDone, this
      );
this.listenTo(
options.controller, 'avatar:uploading:error',
this.uploadingAvatarError, this
      );
    }
  }

uploadingAvatarStart() {
this.originalAvatarMessage = this.$('span.info').html();
this.$('span.notice').html('Uploading avatar...');
  }

uploadingAvatarDone() {
this.$('span.notice').html(this.originalAvatarMessage || '');
  }

uploadingAvatarError() {
this.$('span.notice').html(
'Can't upload image, try again later'
);
  }
}

As the events are triggered by the controller, the view updates the message displayed to the user, so that the user can see if an error occurs, or supplies an uploading message to show what the application is doing.

We should pass the controller instance to the view at creation time:

class ContactEditor {
// ...

showEditor(contact) {
    // ...
varcontactPreview = new ContactPreview({
      controller: this,
      model: contact
    });
  }
}

Uploading a file with AJAX

The Client model receive the blob object, builds the URL to the avatar endpoint, and makes the appropriate calls to the callback objects:

// apps/contacts/models/contact.js
class Contact extends Backbone.Model {
  // ...

uploadAvatar(imageBlob, options) {
    // Create a form object to emulate a multipart/form-data
varformData = new FormData();
formData.append('avatar', imageBlob);

varajaxOptions = {
url: '/api/contacts/' + this.get('id') + '/avatar',
      type: 'POST',
      data: formData,
      cache: false,
contentType: false,
processData: false
    };

    options = options || {};

    // Copy options to ajaxOptions
_.extend(ajaxOptions, _.pick(options, 'success', 'error'));

    // Attach a progress handler only if is defined
    if (options.progress) {
ajaxOptions.xhr = function() {
varxhr = $.ajaxSettings.xhr();

        if (xhr.upload) {
          // For handling the progress of the upload
xhr.upload.addEventListener('progress', event => {
            let length = event.total;
            let uploaded = event.loaded;
            let percent = uploaded / length;

options.progress(length, uploaded, percent);
          }, false);
        }

        return xhr;
      };
    }

$.ajax(ajaxOptions);
  }

  // ...
}

See how the model builds the endpoint from its own data so that the view is decoupled of any server connection. As the multipart/form-data POST is not managed natively by the browser, we should create a FormData object that represents a form data structure, and add an avatar field (the field name that is expecting the server).

They key attribute in the $.ajax() call is processData, which is set to false; you can read the following in the jQuery documentation:

By default, data passed in to the data option as an object (technically, anything other than a string) will be processed and transformed into a query string, fitting to the default content-type "application/x-www-form-urlencoded". If you want to send a DOMDocument, or other non-processed data, set this option to false.

If you don't set this attribute to false, or leave it at the default, jQuery will try transform the formData object and the file will not be sent.

If a progress attribute is set in the options object, we overwrite the original xhr() function called by jQuery to get an XMLHttpRequest object instance; this allow us to listen for the progress event triggered by the browser while uploading the file.

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

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