Adding forms to our application

Now that we know how to work with user input elements, we can add forms to create and edit contacts in our contact management application.

Adding new routes

We need to add three new routes: one to create new contacts, another to edit an existing contact, and the last one to upload the photo of a contact. Let's add them in our root component:

The src/app.js file will be as follows:

export class App { 
  configureRouter(config, router) { 
    this.router = router; 
    config.title = 'Learning Aurelia'; 
    config.map([ 
      { route: '', redirect: 'contacts' }, 
      { route: 'contacts', name: 'contacts', moduleId:        'contact-list',  
        nav: true, title: 'Contacts' }, 
      { route: 'contacts/new', name: 'contact-creation',  
        moduleId: 'contact-edition', title: 'New contact' }, 
      { route: 'contacts/:id', name: 'contact-details',  
        moduleId: 'contact-details' }, 
      { route: 'contacts/:id/edit', name: 'contact-edition',  
        moduleId: 'contact-edition' }, 
      { route: 'contacts/:id/photo', name: 'contact-photo', 
        moduleId: 'contact-photo' }, 
    ]); 
    config.mapUnknownRoutes('not-found'); 
  } 
} 

The three new routes are highlighted in the preceding code snippet.

The positioning is important here. The contact-creation route is placed before the contact-details route because of their respective route property. When trying to find a matching route upon a URL change, the router drills down through the route definitions in the order they were defined in. Since the pattern for contact-details matches any path starting with contacts/ and followed by a second part interpreted as a parameter, the path contacts/new matches this pattern, so the contact-creation route would be unreachable if it was defined later, and the contact-details route would be reached instead with an id parameter equal to new.

A better alternative to relying on the order of the routes would be to change the patterns so there is no possible collision. For example, we could change the pattern of contact-details to something like contacts/:id/details. In such a case, the order of the routes would not matter anymore.

You may have noticed that two of the new routes have the same moduleId. This is because we will use the same component for both creating a new contact and editing an existing one.

Adding links to the new routes

The next step will be to add links leading to the routes we just added. We will first add a link to the contact-creation route in the contact-list component:

src/contact-list.html

 <template> 
  <section class="container"> 
    <h1>Contacts</h1> 
 
    <div class="row"> 
      <div class="col-sm-1"> 
        <a route-href="route: contact-creation" class= "btn btn-primary"> 
          <i class="fa fa-plus-square-o"></i> New 
        </a> 
      </div> 
      <div class="col-sm-2"> 
        <!-- Search box omitted for brevity --> 
      </div> 
    </div> 
    <!--  Contact list omitted for brevity --> 
  </section> 
</template> 

Here we add an a element and leverage the route-href attribute to render the URL for the contact-creation route.

We also need to add links to the contact-photo and the contact-edition routes. We will do this in the contact-details component:

src/contact-details.html

 <template> 
  <section class="container"> 
    <div class="row"> 
      <div class="col-sm-2"> 
        <a route-href="route: contact-photo; params.bind:
          { id: contact.id }"  
           > 
          <img src.bind="contact.photoUrl" class= "img-responsive" alt="Photo"> 
        </a> 
      </div> 
      <div class="col-sm-10"> 
        <template if.bind="contact.isPerson"> 
          <h1>${contact.fullName}</h1> 
          <h2>${contact.company}</h2> 
        </template> 
        <template if.bind="!contact.isPerson"> 
          <h1>${contact.company}</h1> 
        </template> 
        <a class="btn btn-default" route-href="route:
          contact-edition;  
          params.bind: { id: contact.id }"> 
          <i class="fa fa-pencil-square-o"></i> Modify 
        </a> 
      </div> 
    </div> 
    <!-- Rest of template omitted for brevity --> 
  </section> 
</template> 

Here we first refactor the template displaying fullName and company if the contact is a person, by adding an enclosing div and moving the col-sm-10 CSS class from the titles to this div.

Next, we wrap the img element displaying the contact's photo inside an anchor navigating to the contact-photo route, using the contact's id as a parameter.

Lastly, we add another anchor leading to the contact-edition route, using the contact's id as a parameter.

Updating models

In order to reuse code, we will stick with the Contact class and use it in our form component. We will also create classes for phone numbers, email addresses, addresses, and social profiles, so our contact-edition component won't have to know the details of how to create empty instances of those objects.

We need to add the ability to create empty instances of our models, and have all their properties initialized to proper default values. As such, we will add default values for all properties on our model classes.

Lastly, we need to update the ContactfromObject factory method so all list items are properly mapped to instances of our model classes.

src/models.js

export class PhoneNumber { 
  static fromObject(src) { 
    return Object.assign(new PhoneNumber(), src); 
  } 
 
  type = 'Home'; 
  number = ''; 
} 
 
export class EmailAddress { 
  static fromObject(src) { 
    return Object.assign(new EmailAddress(), src); 
  } 
 
  type = 'Home'; 
  address = ''; 
} 
 
export class Address { 
  static fromObject(src) { 
    return Object.assign(new Address(), src); 
  } 
 
  type = 'Home'; 
  number = ''; 
  street = ''; 
  postalCode = ''; 
  city = ''; 
  state = ''; 
  country = ''; 
} 
 
export class SocialProfile { 
  static fromObject(src) { 
    return Object.assign(new SocialProfile(), src); 
  } 
 
  type = 'GitHub'; 
  username = ''; 
} 
 
export class Contact { 
  static fromObject(src) { 
    const contact = Object.assign(new Contact(), src); 
    contact.phoneNumbers = contact.phoneNumbers 
      .map(PhoneNumber.fromObject); 
    contact.emailAddresses = contact.emailAddresses 
      .map(EmailAddress.fromObject); 
    contact.addresses = contact.addresses 
      .map(Address.fromObject); 
    contact.socialProfiles = contact.socialProfiles 
      .map(SocialProfile.fromObject); 
    return contact; 
  } 
 
  firstName = ''; 
  lastName = ''; 
  company = ''; 
  birthday = ''; 
  phoneNumbers = []; 
  emailAddresses = []; 
  addresses = []; 
  socialProfiles = []; 
  note = ''; 
 
  // Omitted snippet... 
} 

Here, we first add classes for a PhoneNumber, an EmailAddress, an Address, and a SocialProfile. Each of those classes has a static fromObject factory method and its properties are properly initialized with default values.

Next, we add the properties of a Contact, initialized with default values, and change its fromObject factory method so the list items are properly mapped to their respective classes.

Creating the form component

Now we can create our new contact-edition component. As mentioned earlier, this component will be used for both creating and editing. It will be able to detect if it is used to create a new contact or to edit an existing one by checking whether it receives an id parameter in its activate callback method. Indeed, the pattern for the contact-creation route defines no parameter, so when our form component gets activated by this route, it won't receive any id parameter. On the other hand, since the pattern for the contact-edition route does define an id parameter, our form component will receive the parameter when activated by this route.

We can do this because, in the scope of our contacts management application, the creation and editing processes are almost identical. However, in many cases, it might be a better design to have separate components for creating and editing.

Activating the view-model

Let's first start with the view-model and the activate callback method:

src/contact-edition.js

import {inject} from 'aurelia-framework'; 
import {ContactGateway} from './contact-gateway'; 
import {Contact} from './models'; 
 
@inject(ContactGateway) 
export class ContactEdition { 
  constructor(contactGateway) { 
    this.contactGateway = contactGateway; 
  } 
 
  activate(params, config) { 
    this.isNew = params.id === undefined; 
    if (this.isNew) { 
      this.contact = new Contact(); 
    } 
    else { 
      return this.contactGateway.getById(params.id).then(contact => { 
        this.contact = contact; 
        config.navModel.setTitle(contact.fullName); 
      }); 
    } 
  } 
} 

Here, we start by injecting an instance of the ContactGateway class into our view-model. Then, in the activate callback method, we first define an isNew property, based on the existence of an id parameter. This property will be used by our component to know if it is being used to create a new contact or to edit an existing one.

Next, based on this isNew property, we initialize the component. If we are creating a new contact, then we simply create a contact property and assign to it a new, empty Contact instance; otherwise, we use the ContactGateway to retrieve the proper contact based on the id parameter and, when the Promise resolves, assign the Contact instance to the contact property and set the document title to the contact's fullName property.

Once the activation cycle completes, the view-model has a contact property properly initialized to a Contact object, and an isNew property indicating if the contact is a new or existing one.

Building the form layout

Next, let's build the template to display the form. This template being pretty big, I will break it down in parts, so you can build it gradually and test it at each step if you want.

The template consists of a header, followed by a form element, which will enclose the rest of the template:

src/contact-edition.html

 <template> 
  <section class="container"> 
    <h1 if.bind="isNew">New contact</h1> 
    <h1 if.bind="!isNew">Contact #${contact.id}</h1> 
 
    <form class="form-horizontal"> 
      <!-- The rest of the template goes in here --> 
    </form> 
  </section> 
</template> 

In the header, we use the isNew property to display either a static title telling the user he is creating a new contact or a dynamic title displaying the id of the contact being edited.

Editing scalar properties

Next, we will add blocks, containing input elements, to edit the firstName, lastName, company, birthday, and note of the contact, inside the form element defined in the previous code snippet:

<div class="form-group"> 
  <label class="col-sm-3 control-label">First name</label> 
  <div class="col-sm-9"> 
    <input type="text" class="form-control" value.bind="contact.firstName"> 
  </div> 
</div> 
 
<div class="form-group"> 
  <label class="col-sm-3 control-label">Last name</label> 
  <div class="col-sm-9"> 
    <input type="text" class="form-control" value.bind="contact.lastName"> 
  </div> 
</div> 
 
<div class="form-group"> 
  <label class="col-sm-3 control-label">Company</label> 
  <div class="col-sm-9"> 
    <input type="text" class="form-control" value.bind="contact.company"> 
  </div> 
</div> 
 
<div class="form-group"> 
  <label class="col-sm-3 control-label">Birthday</label> 
  <div class="col-sm-9"> 
    <input type="date" class="form-control" value.bind="contact.birthday"> 
  </div> 
</div> 
 
<div class="form-group"> 
  <label class="col-sm-3 control-label">Note</label> 
  <div class="col-sm-9"> 
    <textarea class="form-control" value.bind="contact.note"></textarea> 
  </div> 
</div> 

Here, we simply define a form-group for each property to edit. The first three properties are each bound to a text input element. Additionally, the birthday property is bound to a date input, making it easier to edit a date--for browsers supporting it, of course--and the note property is bound to a textarea element.

Editing phone numbers

After this, we need to add editors for lists. Since the data contained in each list is not very complex, we will render inline editors, so the user can edit any field of any item directly, in the minimum number of clicks.

We will discuss more complicated editing models, using dialogs, later in this chapter.

Let's start with the phone numbers:

<hr> 
<div class="form-group" repeat.for="phoneNumber of contact.phoneNumbers"> 
  <div class="col-sm-2 col-sm-offset-1"> 
    <select value.bind="phoneNumber.type" class="form-control"> 
      <option value="Home">Home</option> 
      <option value="Office">Office</option> 
      <option value="Mobile">Mobile</option> 
      <option value="Other">Other</option> 
    </select> 
  </div> 
  <div class="col-sm-8"> 
    <input type="tel" class="form-control" placeholder="Phone number"  
           value.bind="phoneNumber.number"> 
  </div> 
  <div class="col-sm-1"> 
    <button type="button" class="btn btn-danger"  
            click.delegate="contact.phoneNumbers.splice($index, 1)"> 
      <i class="fa fa-times"></i>  
    </button> 
  </div> 
</div> 
<div class="form-group"> 
  <div class="col-sm-9 col-sm-offset-3"> 
    <button type="button" class="btn btn-default" click.delegate="contact.addPhoneNumber()"> 
      <i class="fa fa-plus-square-o"></i> Add a phone number 
    </button> 
  </div> 
</div> 

This phone number list editor can be broken down into parts, the most important of which are highlighted. First, a form-group is repeated for each phoneNumber in the contact's phoneNumbers array.

For each phoneNumber, we define a select element, whose value is bound to the type property of phoneNumber, along with a tel input, whose value is bound to the number property of phoneNumber. Additionally, we define a button whose click event splices the phone number out of the phoneNumbers array of contact, using the current $index, which, as you may remember from the previous chapter, is added to the binding context by the repeat attribute.

Lastly, after the list of phone numbers, we define a button whose click event calls the addPhoneNumber method in contact.

Adding the missing method

One of the buttons we added in the previous template calls a method that is not defined yet. Let's add this method to the Contact class:

src/models.js

//Snippet... 
export class Contact { 
  //Snippet... 
  addPhoneNumber() { 
    this.phoneNumbers.push(new PhoneNumber()); 
  } 
} 

The first method in this code snippet is used to add an empty phone number to the list, by simply pushing a new PhoneNumber instance in the phoneNumbers array.

Editing the other lists

The templates for the other lists, email addresses, addresses, and social profiles, are pretty similar. Only the fields being edited change, but the main concepts--repeating form groups, having a remove button for each item and an add button at the end of the list--are the same.

Let's start with emailAddresses:

<hr> 
<div class="form-group" repeat.for="emailAddress of contact.emailAddresses"> 
  <div class="col-sm-2 col-sm-offset-1"> 
    <select value.bind="emailAddress.type" class="form-control"> 
      <option value="Home">Home</option> 
      <option value="Office">Office</option> 
      <option value="Other">Other</option> 
    </select> 
  </div> 
  <div class="col-sm-8"> 
    <input type="email" class="form-control" placeholder="Email address"  
           value.bind="emailAddress.address"> 
  </div> 
  <div class="col-sm-1"> 
    <button type="button" class="btn btn-danger"  
            click.delegate="contact.emailAddresses.splice($index, 1)"> 
      <i class="fa fa-times"></i>  
    </button> 
  </div> 
</div> 
<div class="form-group"> 
  <div class="col-sm-9 col-sm-offset-3"> 
    <button type="button" class="btn btn-primary"  
            click.delegate="contact.addEmailAddress()"> 
      <i class="fa fa-plus-square-o"></i> Add an email address 
    </button> 
  </div> 
</div> 

This template is very similar to the phone numbers' template. The main difference is that the available types are not exactly the same, and the input type is email.

As you can imagine, the editor for addresses will be a little bigger:

<hr> 
<div class="form-group" repeat.for="address of contact.addresses"> 
  <div class="col-sm-2 col-sm-offset-1"> 
    <select value.bind="address.type" class="form-control"> 
      <option value="Home">Home</option> 
      <option value="Office">Office</option> 
      <option value="Other">Other</option> 
    </select> 
  </div> 
  <div class="col-sm-8"> 
    <div class="row"> 
      <div class="col-sm-4"> 
        <input type="text" class="form-control" placeholder="Number"  
               value.bind="address.number"> 
      </div> 
      <div class="col-sm-8"> 
        <input type="text" class="form-control" placeholder="Street"  
               value.bind="address.street"> 
      </div> 
    </div> 
    <div class="row"> 
      <div class="col-sm-4"> 
        <input type="text" class="form-control" placeholder="Postal code"  
               value.bind="address.postalCode"> 
      </div> 
      <div class="col-sm-8"> 
        <input type="text" class="form-control" placeholder="City"  
               value.bind="address.city"> 
      </div> 
    </div> 
    <div class="row"> 
      <div class="col-sm-4"> 
        <input type="text" class="form-control" placeholder="State"  
               value.bind="address.state"> 
      </div> 
      <div class="col-sm-8"> 
        <input type="text" class="form-control" placeholder="Country"  
               value.bind="address.country"> 
      </div> 
    </div> 
  </div> 
  <div class="col-sm-1"> 
    <button type="button" class="btn btn-danger"  
            click.delegate="contact.addresses.splice($index, 1)"> 
      <i class="fa fa-times"></i>  
    </button> 
  </div> 
</div> 
<div class="form-group"> 
  <div class="col-sm-9 col-sm-offset-3"> 
    <button type="button" class="btn btn-primary"  
            click.delegate="contact.addAddress()"> 
      <i class="fa fa-plus-square-o"></i> Add an address 
    </button> 
  </div> 
</div> 

Here, the left part contains six different inputs, allowing us to edit the various text properties that an address has.

At this point, you probably have some idea what the template for the social profiles looks like:

<hr> 
<div class="form-group" repeat.for="profile of contact.socialProfiles"> 
  <div class="col-sm-2 col-sm-offset-1"> 
    <select value.bind="profile.type" class="form-control"> 
      <option value="GitHub">GitHub</option> 
      <option value="Twitter">Twitter</option> 
    </select> 
  </div> 
  <div class="col-sm-8"> 
    <input type="text" class="form-control" placeholder="Username"  
           value.bind="profile.username"> 
  </div> 
  <div class="col-sm-1"> 
    <button type="button" class="btn btn-danger"  
            click.delegate="contact.socialProfiles.splice($index, 1)"> 
      <i class="fa fa-times"></i>  
    </button> 
  </div> 
</div> 
<div class="form-group"> 
  <div class="col-sm-9 col-sm-offset-3"> 
    <button type="button" class="btn btn-primary"  
            click.delegate="contact.addSocialProfile()"> 
      <i class="fa fa-plus-square-o"></i> Add a social profile 
    </button> 
  </div> 
</div> 

Of course, the methods to add items for each list need to be added to the Contact class:

src/models.js

//Omitted snippet... 
export class Contact { 
  //Omitted snippet... 
  addEmailAddress() { 
    this.emailAddresses.push(new EmailAddress()); 
  } 
 
  addAddress() { 
    this. addresses.push(new Address()); 
  } 
 
  addSocialProfile() { 
    this.socialProfiles.push(new SocialProfile()); 
  } 
} 

As you can see, those methods are almost identical to the ones we wrote previously for the phone numbers. Additionally, the template snippets for each list are also mostly identical to each other. All this redundancy screams for refactoring. We will see in Chapter 5, Making Reusable Components, how we can extract common behaviors and template snippets into a component, which we will reuse to manage each list.

Saving and canceling

The last thing missing for our form to be (at least visually) complete is a Save and a Cancel button at the end of the enclosing form element:

//Omitted snippet... 
<form class="form-horizontal" submit.delegate="save()"> 
  //Omitted snippet... 
  <div class="form-group"> 
      <div class="col-sm-9 col-sm-offset-3"> 
        <button type="submit" class="btn btn-success">Save</button> 
        <a if.bind="isNew" class="btn btn-danger"  
           route-href="route: contacts">Cancel</a> 
        <a if.bind="!isNew" class="btn btn-danger"  
           route-href="route: contact-details;  
           params.bind: { id: contact.id }">Cancel</a> 
      </div> 
    </div> 
</form> 

First, we bind a call to the save method to the form element's submit event, then we add a last form-group containing a submit button named Save.

Next, we add two Cancel links: one displayed when creating a new contact to navigate back to the contacts list, and another displayed when editing an existing contact to navigate back to the contact's details.

We also need to add the save method to the view-model. This method will ultimately delegate to the ContactGateway but, in order to test that everything we've done upto this point, works, let's just write a dummy version of the method:

save() { 
  alert(JSON.stringify(this.contact)); 
} 

At this point, you should be able to run the application and try to create or edit a contact. When clicking the Save button, you should see an alert displaying the contact, serialized as JSON.

Sending data with fetch

We can now add methods to create and update contacts to the ContactGateway class:

src/contact-gateway.js

//Omitted snippet... 
import {HttpClient, json} from 'aurelia-fetch-client'; 
//Omitted snippet... 
export class ContactGateway { 
  //Omitted snippet... 
  create(contact) { 
    return this.httpClient.fetch('contacts',  
      { method: 'POST', body: json(contact) }); 
  } 
 
  update(id, contact) { 
    return this.httpClient.fetch(`contacts/${id}`,  
      { method: 'PUT', body: json(contact) }); 
  } 
} 

The first thing to do is import the json function from fetch-client. This function takes any JS value as an argument and returns a Blob object containing the received parameter serialized as JSON.

Next, we add a create method, which takes a contact as a parameter and calls the HTTP client's fetch method, passing to it the relative URL to call, followed by a configuration object. This object contains properties that will be assigned to the underlying Request object. Here, we specify a method property, telling the client to perform a POST request, and we indicate that the body of the request will be the contact serialized as JSON. Finally, the fetch method returns a Promise, which is returned by our new create method, so callers can react when the request completes.

The update method is very similar. The first difference is the parameters: the contact's id is first expected, followed by the contact object itself. Secondly, the fetch call is slightly different; it sends a request to a different URL, using the PUT method, but its body is the same.

The body of a Fetch Request is expected to be either a Blob, a BufferSource, a FormData, an URLSearchParams, or an USVString object. Documentation about this can be found on the Mozilla Developer Network, at https://developer.mozilla.org/en-US/docs/Web/API/Request/Request.

In order to test that our new methods work, let's replace our dummy save method in the view-model of the contact-edition component with the real deal:

//Omitted snippet... 
import {Router} from 'aurelia-router'; 
 
@inject(ContactGateway, Router) 
export class ContactEdition { 
  constructor(contactGateway, router) { 
    this.contactGateway = contactGateway; 
    this.router = router; 
  } 
 
  // Omitted snippet... 
  save() { 
    if (this.isNew) { 
      this.contactGateway.create(this.contact)  
        .then(() => this.router.navigateToRoute('contacts')); 
    } 
    else { 
      this.contactGateway.update(this.contact.id, this.contact)  
        .then(() => this.router.navigateToRoute('contact-details',  
                    { id: this.contact.id })); 
    } 
  } 
} 

Here, we first import the Router and inject an instance of it in the view-model. Next, we change the body of the save method: if the component is creating a new contact, we first call the create method from ContactGateway, passing the contact object to it, then navigate back to the contacts route when the Promise resolves; otherwise, when the component is editing an existing contact, we first call the update method of ContactGateway, passing the contact's id and the contact object to it, then navigate back to the contact's details when the Promise resolves.

At this point, you should be able to create or update a contact. However, some create or update requests may return responses with a 400 bad Request status. Don't be alarmed; since the HTTP endpoint performs some validation, and our form does not at this point, this is expected to happen-if you leave some field empty, for example. We will add validation to our form later in this chapter, which will prevent this class of error.

Uploading a contact's photo

Now that we can create and edit a contact, let's add a component to upload its photo. This component will be named contact-photo, and will be activated by the route with the same name, which we already added earlier in this chapter.

This component will use a file input element to let the user select an image file on his file system, and will leverage the HTML5 File API along with the Fetch client to send a PUT request containing the selected image file to our HTTP end-point.

Building the template

The template for this component simply reuses a couple of the concepts we already covered:

src/contact-photo.html

 <template> 
  <section class="container"> 
    <h1>${contact.fullName}</h1> 
 
    <form class="form-horizontal" submit.delegate="save()"> 
      <div class="form-group"> 
        <label class="col-sm-3 control-label" for="photo">Photo</label> 
        <div class="col-sm-9"> 
          <input type="file" id="photo" accept="image/*"  
                 files.bind="photo"> 
        </div> 
      </div> 
 
      <div class="form-group"> 
        <div class="col-sm-9 col-sm-offset-3"> 
          <button type="submit" class="btn btn-success">Save</button> 
          <a class="btn btn-danger" route-href="route: contact-details;  
             params.bind: { id: contact.id }">Cancel</a> 
        </div> 
      </div> 
    </form> 
  </section> 
</template> 

Here, we first display the fullName of the contact as the page title. Then, inside a form element whose submit event triggers a save method, we add a file input and buttons to Save or Cancel the photo upload. The file input has an accept attribute, forcing the browser's file selection dialog to display only image files, and its files attribute is bound to the photo property.

Creating the view-model

The view-model looks a lot like the contact-edition view-model, at least when comparing imports, constructors, and activate methods:

src/contact-photo.js

import {inject} from 'aurelia-framework'; 
import {Router} from 'aurelia-router'; 
import {ContactGateway} from './contact-gateway'; 
 
@inject(ContactGateway, Router) 
export class ContactPhoto { 
   
  constructor(contactGateway, router) { 
    this.contactGateway = contactGateway; 
    this.router = router; 
  } 
 
  activate(params, config) { 
    return this.contactGateway.getById(params.id).then(contact => { 
      this.contact = contact; 
      config.navModel.setTitle(this.contact.fullName); 
    }); 
  } 
  save() { 
    if (this.photo && this.photo.length > 0) { 
      this.contactGateway.updatePhoto( 
        this.contact.id,  
        this.photo.item(0) 
      ).then(() => { 
        this.router.navigateToRoute( 
          'contact-details',  
          { id: this.contact.id }); 
      }); 
    } 
  } 
} 

This view-model expects both an instance of ContactGateway and a Router instance to be injected in its constructor. In its activate method, it then loads a Contact instance using its id parameter, and initializes the document title using the fullName for the contact. This is all pretty similar to the contact-edition view-model.

The save method is a little different however. It first checks if a file has been selected; if not, it does nothing for now. Otherwise, it calls the updatePhoto method of the ContactGateway method, passing the contact's id and the selected file to it, and navigates back to the contact's details when the Promise resolves.

Uploading files with fetch

The last step in getting our photo upload feature working is the uploadPhoto method in the ContactGateway class:

src/contact-gateway.js

//Omitted snippet... 
export class ContactGateway { 
  //Omitted snippet... 
  updatePhoto(id, file) { 
    return this.httpClient.fetch(`contacts/${id}/photo`, {  
      method: 'PUT', 
      headers: { 'Content-Type': file.type }, 
      body: file 
    }); 
  } 
} 

Our HTTP backend's contacts/{id}/photo endpoint expects a PUT request, with the proper Content-Type header and the image binary as its body. This is exactly what the call to fetch does here: it uses the file argument, which is expected to be an instance of the File class, and sets the Content-Type header using its type property, then sends the file itself as the request body.

As mentioned earlier, the File class is part of the HTML5 File API. The Mozilla Developer Network has extensive documentation about this API. Details about the File class can be found at https://developer.mozilla.org/en-US/docs/Web/API/File.

As usual, the updatePhoto method returns the Promise resolved by the HTTP request, so the caller can act when the operation is completed.

At this point, you should be able to run the application and update the photo of a contact by uploading a new image file.

Deleting a contact

At this point, our application allows us to create, read, and update contacts. Obviously, one of the letters from create, read, update, delete (CRUD) is missing here: we can't delete a contact yet. Let's quickly implement this feature.

First, let's add a Delete button to the contact's details component:

src/contact-details.html

 <template> 
  <section class="container"> 
    <div class="row"> 
      <div class="col-sm-2"> 
        <!-- Omitted snippet... --> 
      </div> 
      <div class="col-sm-10"> 
        <!-- Omitted snippet... --> 
        <a class="btn btn-default" route-href="route: contact-edition;  
          params.bind: { id: contact.id }"> 
          <i class="fa fa-pencil-square-o"></i> Modify 
        </a> 
        <button class="btn btn-danger" click.delegate="tryDelete()"> 
          <i class="fa fa-trash-o"></i> Delete 
        </button> 
      </div> 
    </div> 
    <!-- Rest of template omitted for brevity --> 
  </section> 
</template> 

The new Delete button will call the tryDelete method when clicked:

src/contact-details.js

//Omitted snippet... 
export class ContactDetails { 
  //Omitted snippet... 
  tryDelete() { 
    if (confirm('Do you want to delete this contact?')) { 
      this.contactGateway.delete(this.contact.id) 
        .then(() => { this.router.navigateToRoute('contacts'); }); 
    } 
  } 
} 

The tryDelete method first asks the user to confirm deletion, then calls the gateway's delete method with the contact's id. When the returned Promise resolves, it navigates back to the contacts list.

Lastly, the ContactGateway class' delete method just performs a Fetch call to the backend's proper path, using the DELETE HTTP method:

src/contact-gateway.js

//Omitted snippet... 
export class ContactGateway { 
  //Omitted snippet... 
  delete(id) { 
    return this.httpClient.fetch(`contacts/${id}`, { method: 'DELETE' }); 
  } 
} 

At this point, if you click on the Delete button for a contact and approve the confirmation dialog, you should be redirected to the contacts list and the contact should be gone.

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

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