As we have seen so far, to upload and attach a file to a resource, it must already exist. How we can create a resource with a file attached? How can we create a contact that includes an avatar image?
To do so, we will need to create the resource in two steps. In the first step, we create the resource itself, and then in a second step we can upload all files we want to that resource. Yes, it's not possible to do this in a single server connection, at least without encoding the files you want to send:
The preceding figure shows how the process is done. Note that the model is responsible for handling these connections while the controller orchestrates the order of the communication and error handling. As we have seen previously, the ContactEditor
triggers several events that the view can use to show to the user what's happening.
The views can be left as is; we should only modify the ContactEditor
controller by changing how the saveContact()
method behaves. However, we want to keep the feature of uploading the image as the user makes the selection. If the Contact model is new, this feature will break the application because no valid endpoint exists to upload the avatar:
class ContactEditor { // ... showEditor(contact) { // ... // When avatar is selected, we can save it inmediatly if the // contact already exists on the server, otherwise just // remember the file selected this.listenTo(contactPreview, 'avatar:selected', blob => { this.avatarSelected = blob; if (!contact.isNew()) { this.uploadAvatar(contact); } }); } }
When an avatar is selected, instead of immediately uploading the file to the server, we check if the contact is new or not. If the model is not new, we can perform the upload by calling the uploadAvatar()
method; otherwise, we keep a reference to the blob object in the avatarSelected
attribute that the uploadAvatar()
method will use when it is called.
The saveContact()
method is responsible for orchestrating the algorithm described in the previous figure:
// apps/contacts/contactEditor.js class ContactEditor { saveContact(contact) { varphonesData = this.phones.toJSON(); varemailsData = this.emails.toJSON(); contact.set({ phones: phonesData, emails: emailsData }); if (!contact.isValid(true)) { return; } varwasNew = contact.isNew(); // The avatar attribute is read-only if (contact.has('avatar')) { contact.unset('avatar'); } function notifyAndRedirect() { // Redirect user to contact list after save App.notifySuccess('Contact saved'); App.router.navigate('contacts', true); } contact.save(null, { success: () =>{ // If we are not creating an user it's done if (!wasNew) { notifyAndRedirect(); return; } // On user creation send the avatar to the server too this.uploadAvatar(contact, { success: notifyAndRedirect }); }, error() { // Show error message if something goes wrong App.notifyError('Something goes wrong'); } }); } // ... }
Before calling the save()
method in the Contact model, it's necessary to save whether the model is new or not; if we call this method after the save, the isNew()
method will always return true
.
If the model wasn't new, then any changes in the avatar image were already uploaded by the 'avatar:selected'
event handler, so we don't need to upload the image again. But if the image was new, then we should upload the avatar by calling the uploadAvatar()
method; note that the method accepts an options
object to register callbacks. This is necessary to provide feedback to the user; when the upload is done it calls the notifyAndRedirect()
function to show a notification message and returns to the list of contacts.
We will need to change the implementation of uploadAvatar()
to include the callbacks described earlier and to instead receive the blob as soon as it uses the avatarSelected
attribute:
// apps/contacts/contactEditor.js uploadAvatar(contact, options) { // Tell to others that upload will start this.trigger('avatar:uploading:start'); contact.uploadAvatar(this.avatarSelected, { progress: (length, uploaded, percent) => { // Tell to others that upload is in progress this.trigger('avatar:uploading:progress', length, uploaded, percent); if (options &&_.isFunction(options.success)) { options.success(); } }, 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 method is basically the same; we just add the options
callbacks and change the source of the blob object.
3.149.228.138