Implementing client-side the contact modification (8/9)


Go back to the table of contents for
Developing a Contact Management Application with Angular 1.5X and Java


Here are the tasks to implement the contact modification use case from the client side :



Preview of the content that we want to get

preview-screen-1

When the edit button is clicked on, we display the contact edition at the right that overlaps the contacts list :

preview-screen-2

Use case presentation

The contact modification should be a provided feature from the contactList component.

Source code layout

 source-code

Extracting enumerated values common both to contactCreation and to the new component we will create : contactEdit

Here is the concerned actual code in ContactCreationController:

    function ContactCreationController($scope, contactService) {
        var ctrl = this;
 
        ctrl.sexes = [{
                value: 'MALE'
            },
            {
                value: 'FEMALE'
            },
            {
                value: 'UNKNOWN'
            }];

In the contactEdit controller, we will also need to provide the enumerated values for the sex input field. Instead of duplicating it in the contactEdit controller, we will create a plain JavaScript object that will provide a method to retrieve these enum values.

Here is the contact-factory.js content, a new JavaScript file we introduce to address this need :

 var contactNgUtil = (function () {
 
     var sexes = [{
             value: 'MALE'
            },
         {
             value: 'FEMALE'
            },
         {
             value: 'UNKNOWN'
            }];
 
 
     return {
 
         getSexes: function () {
             return sexes;
         }
 
     }
     // END IIFE
 })();

Now, we could modify ContactCreationController in this way :

ctrl.sexes = contactNgUtil.getSexes();

Replacing the contactCreation.html template by contactCreationOrEdit.html, a more general template usable both by the contact creation and the contact modification components

Actually, the component used to represent a contact creation is very close to the component we need to represent a contact modification.
Here is the displayed page that holds the contactCreation component where I added some annotations to show the differences between this content and which one expected for the contact modification page :

contact-creation-vs-contact-modification-screen We can see that an additional data is transmitted in the contact modification component : the contact id. Indeed, when we create a contact, the id is not exist yet while when we modify a contact, we have to transmit the contact id to the backend in order that it is able to update the concerned contact.
We can also see that three textual information differs between the two use cases : the text header and the text of the two buttons.
Here are the modifications we will do.

– text header :

<!-- heading -->
<div class="panel-heading">
	<span class="lead">Contact Creation</span>
</div>

becomes

<!-- heading -->
<div class="panel-heading">
	<span class="lead">{{$ctrl.title}}</span>
</div>

– the contact id transmitted that was not transmitted:

<form name="myForm" class="form-horizontal">

is now transmitted if it exists :

<form name="myForm" class="form-horizontal">
     <input type="hidden" ng-model="$ctrl.contact.id" />


– buttons text :

<div class="form-group col-md-12 ">
	<div class="text-center ">
		<button type="button" ng-click="$ctrl.submitAction() " class="btn btn-primary btn-sm " ng-disabled="myForm.$invalid ">Create</button>
		<div class="divider" />
		<button type="button" ng-click="$ctrl.resetAction() " class="btn btn-warning btn-sm ">Clear</button>
	</div>
</div>

becomes

<div class="form-group col-md-12 ">
	<div class="text-center ">
		<button type="button" ng-click="$ctrl.submitAction() " class="btn btn-primary btn-sm" ng-disabled="myForm.$invalid ">{{$ctrl.btnFormValidationLbl}}</button>
		<div class="divider" />
		<button type="button" ng-click="$ctrl.resetAction() " class="btn btn-warning btn-sm">{{$ctrl.btnFormRevertLbl}}</button>
	</div>
</div>

We can see that except the contact id, all displayed texts that differ between contact creation and contact modification components are not hardcoded any longer but displayed by some properties of the controller associated to the template. So, contactCreation component and contactEdit component will be distinct components with each one their own controller that values these properties according to their needs.

Here is the full updated contactCreationOrEdit.html file :

<div class="panel panel-primary">
 
    <!-- heading -->
    <div class="panel-heading">
        <span class="lead">{{$ctrl.title}}</span>
    </div>
 
    <!-- body -->
    <div class="panel-body my-panel-body">
 
        <form name="myForm" class="form-horizontal">
 
                <input type="hidden" ng-model="$ctrl.contact.id" />
                <div class="form-group col-md-12">
                    <label class="col-md-3 control-label">First Name *</label>
                    <div class="col-md-7">
                        <input type="text" autocomplete="on" ng-model="$ctrl.contact.firstName" name="firstName" placeholder="Enter your First Name" required ng-minlength="2" class="form-control input-sm" />
                        <div class="has-error" ng-show="myForm.firstName.$dirty" ng-messages="myForm.firstName.$error">
                            <div ng-messages-include="pages/errors.html"></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12">
                    <label class="col-md-3 control-label">Last Name *</label>
                    <div class="col-md-7">
                        <input type="text" autocomplete="on" ng-model="$ctrl.contact.lastName" name="lastName" class="form-control input-sm" placeholder="Enter your Last Name" required ng-minlength="2" />
                        <div class="has-error" ng-show="myForm.lastName.$dirty" ng-messages="myForm.lastName.$error" role="alert">
                            <div ng-messages-include="pages/errors.html"></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12">
                    <label class="col-md-3 control-label">Birthday</label>
                    <div class="col-md-7">
                        <input type="date" autocomplete="on" ng-model="$ctrl.contact.birthday" name="birthday" class="form-control input-sm" placeholder="Enter your Birthday" />
                        <div class="has-error" ng-show="myForm.birthday.$dirty" ng-messages="myForm.birthday.$error" role="alert">
                            <div ng-messages-include="pages/errors.html"></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12">
                    <label class="col-md-3 control-label">Email *</label>
                    <div class="col-md-7">
                        <input type="email" autocomplete="on" ng-model="$ctrl.contact.email" name="email" class="form-control input-sm" placeholder="Enter your Email" required />
                        <div class="has-error" ng-show="myForm.email.$dirty" ng-messages="myForm.email.$error" role="alert">
                            <div ng-messages-include="pages/errors.html"></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12">
                    <label class="col-md-3 control-label">Phone</label>
                    <div class="col-md-7">
                        <input type="text" autocomplete="on" ng-model="$ctrl.contact.phone" name="phone" class="form-control input-sm" placeholder="Enter your Phone" ng-minlength="8" />
                        <div class="has-error" ng-show="myForm.phone.$dirty" ng-messages="myForm.phone.$error" role="alert">
                            <div ng-messages-include="pages/errors.html"></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12">
                    <label class="col-md-3 control-label">Sex *</label>
                    <div class="col-md-7">
                        <select id="sex " ng-model="$ctrl.contact.sex " name="sex " class="form-control input-sm " required>
                            <option ng-repeat="option in $ctrl.sexes " value="{{option.value}} ">{{option.value}}</option>
                        </select>
                        <div class="has-error" ng-show="myForm.sex.$dirty " ng-messages="myForm.sex.$error " role="alert ">
                            <div ng-messages-include="pages/errors.html "></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12 ">
                      <div class="text-center ">
                      <label>Postal Address</label>
                      </div>
                </div>
 
                <div class="form-group col-md-12 ">
                    <label class="col-md-3 control-label ">Address Line</label>
                    <div class="col-md-7 ">
                        <input type="text " autocomplete="on " name="addressLine " ng-model="$ctrl.contact.address.addressLine " class=" form-control input-sm " placeholder="Enter the Address Line " ng-minlength="2 " />
                        <div class="has-error" ng-show="myForm.addressLine.$dirty " ng-messages="myForm.addressLine.$error " role="alert ">
                            <div ng-messages-include="pages/errors.html "></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12 ">
                    <label class="col-md-3 control-label ">City</label>
                    <div class="col-md-7 ">
                        <input type="text " autocomplete="on " name="city " ng-model="$ctrl.contact.address.city " class="form-control input-sm " placeholder="Enter the city " ng-minlength="2 " />
                        <div class="has-error" ng-show="myForm.city.$dirty " ng-messages="myForm.city.$error " role="alert ">
                            <div ng-messages-include="pages/errors.html "></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12 ">
                    <label class="col-md-3 control-label ">Zip</label>
                    <div class="col-md-7 ">
                        <input type="text " autocomplete="on " name="zip " ng-model="$ctrl.contact.address.zip " class="form-control input-sm " placeholder="Enter the zip " ng-minlength="3 " />
                        <div class="has-error" ng-show="myForm.zip.$dirty " ng-messages="myForm.zip.$error " role="alert ">
                            <div ng-messages-include="pages/errors.html "></div>
                        </div>
                    </div>
                </div>
 
                <div class="form-group col-md-12 ">
                    <label class="col-md-3 control-label ">Country</label>
                    <div class="col-md-7 ">
                        <input type="text " autocomplete="on " name="country " ng-model="$ctrl.contact.address.country " class="form-control input-sm " placeholder="Enter the country " ng-minlength="3 " />
                        <div class="has-error" ng-show="myForm.country.$dirty " ng-messages="myForm.country.$error " role="alert ">
                            <div ng-messages-include="pages/errors.html "></div>
                        </div>
                    </div>
                </div>
 
 
                <div class="form-group col-md-12 ">
                    <div class="text-center ">
                        <button type="button" ng-click="$ctrl.submitAction() " class="btn btn-primary btn-sm" ng-disabled="myForm.$invalid ">{{$ctrl.btnFormValidationLbl}}</button>
                        <div class="divider" />
                        <button type="button" ng-click="$ctrl.resetAction() " class="btn btn-warning btn-sm">{{$ctrl.btnFormRevertLbl}}</button>
                    </div>
                </div>            
        </form>
    </div>
</div>

Modifying the contactCreation component to comply with the contactCreationOrEdit.html template

First, we modify the templateUrl value specified in the component declaration to use contactCreationOrEdit.html. We locate it in a common folder.

templateUrl: 'components/createContact/contactCreation.html?' + new Date()

becomes

templateUrl: 'components/common/contactCreationOrEdit.html?' + new Date()

Then, we define and value the properties used by the template to display text that differs according to the owner component.
In the controller factory function, we add these properties and assign to them the expected values :

function ContactCreationController($scope, contactService) {
	var ctrl = this;
         . . . 
	// dynamic properties for the common template
	ctrl.title = 'Contact Creation';
	ctrl.btnFormValidationLbl = 'Create';
	ctrl.btnFormRevertLbl = 'Clear';
	// end dynamic properties for the common template
         . . .

Here is the full updated contactCreation.js file :

(function () {
    'use strict';
    //IIFE
 
 
    function ContactCreationController($scope, contactService) {
        var ctrl = this;
 
        ctrl.sexes = contactNgUtil.getSexes();
 
        // dynamic properties for the common template
        ctrl.title = 'Contact Creation';
        ctrl.btnFormValidationLbl = 'Create';
        ctrl.btnFormRevertLbl = 'Clear';
        // end dynamic properties for the common template
 
        this.$onInit = function () {
            ctrl.contact = ctrl.createEmptyContact();      
        }
 
        ctrl.submitAction = function () {
 
            console.log('Saving New Contact', ctrl.contact);
 
            contactService.createContact(ctrl.contact)
                .then(
                    function (newId) {
                        ctrl.contact.id = newId;
                        ctrl.onContactAdded({
                            createdContact: ctrl.contact
                        });
                        ctrl.resetAction();
                    },
                    function (errResponse) {
                        ctrl.onError();
                    }
                );
        }
 
        ctrl.createEmptyContact = function () {
            var contact = {
                id: null,
                firstName: '',
                lastName: '',
                birthday: null,
                email: '',
                phone: '',
                sex: '',
                address: {
                    addressLine: '',
                    city: '',
                    zip: '',
                    country: ''
                }
            }
            return contact;
        }        
 
        ctrl.resetAction = function () {
            ctrl.contact = ctrl.createEmptyContact();
            $scope.myForm.$setPristine(); // reset Form            
        }
 
 
    };
 
 
    angular.module('myContactApp')
        .component('contactCreation', {
            bindings: {
                onContactAdded: '&',
                onError: '&'
            },
            templateUrl: 'components/common/contactCreationOrEdit.html?' + new Date(),
            controller: ContactCreationController
        })
 
 
    //END IIFE
})();

Creating contactEdit : the contact modification component

  •  Specify suitable values for the template :
// dynamic properties for the common template
ctrl.title = 'Contact Edit';
ctrl.btnFormValidationLbl = 'Update';
ctrl.btnFormRevertLbl = 'Cancel';
// end dynamic properties for the common template
  • Set the communication channel between sibling components

As explained earlier, the contactEdit component is a child of the contactsManagementView component.

contactsManagementView owns now two children : contactList and contactEdit.

The contactList provides the edit button on each displayed contact row to trigger the display of the contact modification form provided by the contactEdit component.

The sibling components need to communicate between them but they don’t know each other and anyway, they should not.
Otherwise it would increase their responsibilities by making sibling components dependent between them. Which is undesirable as it decreases their readability, maintainability and reusability.

A better way to handle this concern is using the parent component as an intermediary between these components. The parent component reduces the coupling between its child components. It makes the parent less reusable but it is not a problem as the components performing components aggregation are not primarily designed to be reusable but rather to be specific.
Indeed, contactsManagementView will not be reused while contactList  and contactEdit are.

We have multiple ways to perform the communication from a child to the parent and then from the parent to another child, among :

– api registering with two way binding (elegant but not the most effective way)

– api registering with output binding (more boiler plate code but more effective)

– shared data stored in the parent component. It is more relevant when we want to share  data between siblings than when we want to do a processing for a child, triggered from another child.

To provide the contactEdit API to the contactsManagementView component, we will use the api registering with output binding.
As soon as the initialization of the contactEdit component,  contactEdit invokes the parent callback with as parameter its own API.
So, the parent gets a way to communicate with contactEdit .

Here is the $onInit() method of contactEdit :

        this.$onInit = function () {
            ctrl.api = {};
            ctrl.api.edit = ctrl.editAction;
            ctrl.onInit({api: ctrl.api});            
        }

We add a single function in the api property : the editAction method of the controller.

And in its declaration, contactEdit specifies a custom onInit output binding :

   angular.module('myContactApp')
        .component('contactEdit', {
            bindings: {
                ... 
                onInit : '&'
            },
            templateUrl: 'components/common/contactCreationOrEdit.html?' + new Date(),
            controller: ContactEditController
        })

The parent component, contactsManagementView, is able now to invoke editAction, the contactEdit method that edits a contact.

    • Updating the contact by requesting the backend

In contactEdit , when the user clicks on the update button, we send the request to the backend and we handle the result as for the previous use cases. Similarly to contact creation and contact deletion use cases, we invoke the onError() parent callback when an error occurs and the onUpdate() parent callback when the update has succeeded.

Below are these two output binding :

    angular.module('myContactApp')
        .component('contactEdit', {
            bindings: {
                ...
                onError: '&',
                onUpdate: '&',
                ...               
            },

Here is the method associated to this processing :

        ctrl.submitAction = function () {
 
            console.log('Updating Contact', ctrl.contact);
 
            contactService.updateContact(ctrl.contact, ctrl.contact.id)
 
            .then(
                function () {                    
                    ctrl.onUpdate({"contact": ctrl.contact});                    
                },
                function (errResponse) {
                    ctrl.onError();
                }
            );
        }

      • Cancelling the contact edition

When the user clicks on the cancel button, we invoke the onCancel() parent callback to notify it.
Cancelling a contact edition should not change the state of the concerned contact and should also hide the contactEdit component.
The contactEdit display is done by the parent component. So logically, the reverse operation (hiding) should also be performed by the parent component.

So, we add another output binding in contactEdit :

    angular.module('myContactApp')
        .component('contactEdit', {
            bindings: {
                ...
                onCancel: '&',
                ...
            },


Here is the method associated to this processing :

        ctrl.resetAction = function () {                    
            ctrl.onCancel();
        }

Here is the full contactEdit.js file :

(function () {
    'use strict';
    //IIFE
 
 
    function ContactEditController($scope, contactService) {
        var ctrl = this;
        ctrl.sexes = contactNgUtil.getSexes();
 
        // dynamic properties for the common template
        ctrl.title = 'Contact Edit';
        ctrl.btnFormValidationLbl = 'Update';
        ctrl.btnFormRevertLbl = 'Cancel';
        // end dynamic properties for the common template
 
        this.$onInit = function () {
            ctrl.api = {};
            ctrl.api.edit = ctrl.editAction;
            ctrl.onInit({api: ctrl.api});            
        }
 
        ctrl.submitAction = function () {
 
            console.log('Updating Contact', ctrl.contact);
 
            contactService.updateContact(ctrl.contact, ctrl.contact.id)
 
            .then(
                function () {                    
                    ctrl.onUpdate({"contact": ctrl.contact});                    
                },
                function (errResponse) {
                    ctrl.onError();
                }
            );
        }
 
        ctrl.editAction = function (contact) {
            ctrl.contact = contact;
        };
 
        ctrl.resetAction = function () {                    
            ctrl.onCancel();
        }
 
 
    };
 
 
    angular.module('myContactApp')
        .component('contactEdit', {
            bindings: {
                onError: '&',                
                onCancel: '&',
                onUpdate: '&',                
                onInit : '&'
            },
            templateUrl: 'components/common/contactCreationOrEdit.html?' + new Date(),
            controller: ContactEditController
        })
 
 
    //END IIFE
})();

Updating the contactList component

As discussed during the previous point, the contactEdit API is used by the parent component.
But in fact the API use results from the contactList component that sends a callback to its parent to edit a specific contact.  So, we have to set this communication in the contactList component.
Besides, when a contact is updated, the contactList component should be aware about it.
So contactList should also provide an API to its parent in order to allow to refresh the list of displayed contacts.

  •  Notify the parent component when a contact edition is requested

It requires an output communication (& binding).
We add the following binding in the contactList component declaration :
onEditRequest : '&'.

Here is the method invoked when the user click on the edit button :

 ctrl.editAction = function (contact) {
      var copyContact = angular.copy(contact);
      ctrl.onEditRequest({"contact" : copyContact});
 }

We do a defensive copy of the contact to edit in order to prevent any side effect during contact edition. For example, in contactEdit, an invalid value for a property or a property modification that is followed by a cancelling operation should not change the contact state in contactList.

  •  Provide an API to its parent to allow this last one to refresh the contact List

When a contact was successful updated by the contactEdit component, we have seen that contactEdit notifies by callback its parent :

contactService.updateContact(ctrl.contact, ctrl.contact.id)
 
.then(
	function () {                    
		ctrl.onUpdate({"contact": ctrl.contact});                    
	},
	. . .
);

The parent may display the successful message in the notification header but overall it  updates the contact in contactList. Remember what we have just seen : the contact passed to the contactEdit component is a copy and not the original object stored in the contactList component. So, contactList has to publish an API to its parent.

In the parent to child communication between contactsManagementView and contactEdit, we have chosen to use the API registering with output binding. 
A less efficient but more elegant way to address this need is using the API registering with two way binding.
For the communication between contactsManagementView and contactListwe will use this other way. It may be interesting to show it but you should not abuse this way of doing as it uses a two way binding.
We add the following binding in the contactList component declaration : 
api : '='. 
Here is the change in the component $onInit() method :

this.$onInit = function () {
        ... 
	ctrl.api = {};
	ctrl.api.refreshExistingContact = ctrl.refreshExistingContact;
};

Contrary to the solution relying on the api registering with output binding, this time we don’t need to invoke the parent callback to register the API of the child component as the parent component binds in its scope directly the API object of the  contactList component.

At last, here is the refreshExistingContact() method in contactList, exposed by the API and provided to the parent component :

ctrl.refreshExistingContact = function(contact){
	console.log("refresh contact");
 
	 for(var i = 0; i < ctrl.contacts.length; i++){
		 var currentContact = ctrl.contacts[i];
 
		   if (contact.id == currentContact.id){
			   for(var j in contact) currentContact[j] = contact[j];
		   }
	 }		
}

The method is rather simple. It takes as parameter the modified contact and it looks for it in the contacts property of the controller. When the contact is found, we iterate all properties in the modified contact and we use them to overwrite old values in the contact object of the contacts property of ContactListController.

Here is the full updated contactList.js file :

(function () {
    'use strict';
    //IIFE
 
 
    function ContactListController($scope, contactService) {
 
        var ctrl = this;
        ctrl.contacts = [];
        ctrl.addressMode = 'LONG';
 
        this.$onInit = function () {
            ctrl.fetchAllContacts();
  		  	ctrl.api = {};
  		  	ctrl.api.refreshExistingContact = ctrl.refreshExistingContact;
        };
 
        ctrl.fetchAllContacts = function () {
            contactService.fetchAllContacts()
                .then(
                    function (contacts) {
                        ctrl.contacts = contacts;
                    },
                    function (errResponse) {
                        ctrl.onError();
                    }
                );
        }
 
        ctrl.refreshExistingContact = function(contact){
        	console.log("refresh contact");
 
	      	 for(var i = 0; i < ctrl.contacts.length; i++){
	      		 var currentContact = ctrl.contacts[i];
 
	      		   if (contact.id == currentContact.id){
	      			   for(var j in contact) currentContact[j] = contact[j];
	      		   }
	         }		
        }                
 
 
        /**
              user action
          */
        ctrl.toggleAddressMode = function () {
            if (ctrl.addressMode == 'LONG') {
                ctrl.addressMode = 'SHORT';
            } else {
                ctrl.addressMode = 'LONG';
            }
        }
 
        ctrl.isShortAddressMode = function () {
            if (ctrl.addressMode == 'SHORT') {
                return true;
            }
            return false;
        }
 
        ctrl.isFullAddressMode = function () {
            return !ctrl.isShortAddressMode();
        }
 
        ctrl.deleteAction = function (contact) {
            var index = ctrl.contacts.indexOf(contact);
 
            if (index == -1) {
                return;
            }
 
            contactService.deleteContact(contact.id)
                .then(
                    function () {
                        ctrl.contacts.splice(index, 1);
                        ctrl.onContactDeleted({
                            "contactId": contact.id
                        });
                    },
                    function (errResponse) {
                        ctrl.onError();
                    }
                );
 
        }
 
        ctrl.editAction = function (contact) {
           var copyContact = angular.copy(contact);
           ctrl.onEditRequest({"contact" : copyContact});
        }        
    }
 
 
 
    angular.module('myContactApp').component('contactList', {
 
        templateUrl: 'components/manageContacts/contactList.html?' + new Date(),
        controller: ContactListController,
        bindings: {
            onError: '&',
            onEditRequest : '&',
            onContactDeleted : "&",
            api : '='
        }
    });
 
 
 
    //END IIFE
})();

Updating the view component (contactsManagementView) to add the contactEdit component and handle the communication between components

We will add many things in contactsManagementView.
Now, it is a central component that presents multiple callback functions that may call functions of its child components via their API but also performs showing/hiding of the contactEdit component.

  • Updating the contactsManagementView template to display both contactEdit and contactList components

We update the contactList component instantiation and we add and instantiate a contactEdit component with all required attributes.

Here are these modifications on contactsManagementView.html template:

    <contact-list on-error="$ctrl.displayErrorMsg(msg)" 
                     on-edit-request="$ctrl.handleEditRequest(contact)" 
                     on-contact-deleted="$ctrl.contactDeleted(contactId)"
                     api="$ctrl.apiContactList"> 
    </contact-list>
 
    <contact-edit  on-error="$ctrl.displayErrorMsg(msg)"
                      on-cancel="$ctrl.contactEditCanceled()"
                      on-update="$ctrl.contactUpdated(contact)"                        
                      on-init="$ctrl.registerApiContactEdit(api)">
    </contact-edit>

We will discuss about how the controller side uses these bindings in the next points.

  • Updating the contactsManagementView controller to register the contactEdit API

In the template, you can see how the contactEdit associates its API registering method to the registerApiContactEdit() method of the contactsManagement controller :
on-init="$ctrl.registerApiContactEdit(api)".
It relies on an output binding (&) and performs the registering  in two times.
First, during its initialization phase, the contactEdit component calls the onInit() method with as parameter its api (what we have seen early in the contactEdit creation point):

this.$onInit = function () {
	ctrl.api = {};
	ctrl.api.edit = ctrl.editAction;
	ctrl.onInit({api: ctrl.api});            
}

ctrl.onInit({api: ctrl.api}) invocation results to the callback invocation of its parent (contactsManagement), that is registerApiContactEdit(). It stores the contactEdit API in a apiContactEdit property of the controller.

contactsManagementView.js snippet :

ctrl.registerApiContactEdit = function (api) {
	ctrl.apiContactEdit = api; 
}

  • Updating the contactsManagementView controller to implement the callback received from contactList when an contact edition is requested

With the contactEdit API, contactsManagementView has the ability to call the edit() method of its contactEdit child component when a contact edition is requested :

ctrl.handleEditRequest = function(contact){
	console.debug("handleEditRequest with contact "+ contact);
	ctrl.showContactEdit();
	ctrl.apiContactEdit.edit(contact);
}

Before invoking the edit() method of the contactEdit API, we invoke showContactEdit(). It is a method that we will add in contactsManagementView to make visible the contactEdit component.
We will discuss about it when we will introduce the Material UI sidebar that will contain the contactEdit component.

  • Updating the contactsManagementView controller to implement the callback when the contact edition is successful

Before invoking the refreshExistingContact() method of the ContactList API to refresh the updated contact, we invoke hideContactEdit(). It is a method that we will add in contactsManagementView to hide the contactEdit component.
Like showContactEdit() we will discuss about it soon.

ctrl.contactUpdated = function (contact){
	ctrl.hideContactEdit();
	ctrl.apiContactList.refreshExistingContact(contact);
}

  • Updating the contactsManagementView controller to implement the callback when the contact edition is cancelled

ctrl.contactEditCanceled = function (){
	ctrl.hideContactEdit();
}
  • Updating the contactsManagementView controller to set the contactList API.

In the template, you can see how the contactList associates its API to the API property of the contactsManagementView controller : api="$ctrl.apiContactList".
It relies on an two-way binding (=) that spares an explicit registering of the child component API to the parent component.

  • Adding the contactEdit component in a MaterialUI sidebar

The contactEdit component will be by default hidden. When the edit button for a contact row is clicked on, a side bar containing the contactEdit component is displayed at right and overlaps in a some part the contactList component.

Here is the full updated contactsManagementView.html :

<div class="row">
  <div class="col-md-12">
    <ng-include src="'components/common/baseParentComponentHeaderTemplate.html'"></ng-include>
  </div>
</div>
 
<div class="row">
 
  <div class="col-md-12">
 
     <section>
 
        <div>
	      <contact-list on-error="$ctrl.displayErrorMsg(msg)" 
                        on-edit-request="$ctrl.handleEditRequest(contact)" 
                        on-contact-deleted="$ctrl.contactDeleted(contactId)"
                        api="$ctrl.apiContactList"> 
           </contact-list>
        </div>
 
        <md-sidenav class="md-sidenav-right md-whiteframe-4dp" md-component-id="rightComponent"> 
           <contact-edit on-error="$ctrl.displayErrorMsg(msg)"
                         on-cancel="$ctrl.contactEditCanceled()"
                         on-update="$ctrl.contactUpdated(contact)"                        
                         on-init="$ctrl.registerApiContactEdit(api)">
           </contact-edit>
        </md-sidenav>
 
     </section>
  </div>
</div>

The md-component-id of the md-sidenav matters as it gives a way to retrieve and to manipulate the sidebar.
In the contactsManagementView component we add two methods we have discussed just above : one to make visible the sidebarand another one to hide it :

ctrl.showContactEdit = function () {		    
	$mdSidenav('rightComponent').open()
	  .then(function () {
		console.debug("showContactEdit done");
	  });   
}
 
ctrl.hideContactEdit =  function () {
	$mdSidenav('rightComponent').close()
	  .then(function () {
		console.debug("hideContactEdit done");
	  });
}

As the contactEdit component is a child component of the sidebar, making visible or hiding it also applies to contactEdit.

You may notice that the open() and close() functions of the sidebar component have callbacks. In our use case, we use only them to log the event.

Here is the full updated contactsManagementView component:

(function () {
    'use strict';
    //IIFE
 
    function ContactsManagementViewController(baseParentComponentController, $mdSidenav) {
        var ctrl = this;
        angular.extend(ContactsManagementViewController.prototype, baseParentComponentController);
 
		ctrl.registerApiContactEdit = function (api) {
			ctrl.apiContactEdit = api; 
		}
 
		ctrl.showContactEdit = function () {		    
			$mdSidenav('rightComponent')
			  .open()
			  .then(function () {
			    console.debug("showContactEdit done");
			  });   
		}
 
		ctrl.hideContactEdit =  function () {
			$mdSidenav('rightComponent').close()
			  .then(function () {
			  	console.debug("hideContactEdit done");
			  });
		}
 
		ctrl.contactAdded = function (createdContact) {
            ctrl.displaySuccessMsg("Contact with id " + createdContact.id + " added.");
        }
 
		ctrl.contactDeleted = function (contactId) {
		    ctrl.displaySuccessMsg("Contact with id " + contactId + " deleted.");
		}        
 
		ctrl.handleEditRequest = function(contact){
			console.debug("handleEditRequest with id "+ contact);
			ctrl.showContactEdit();
			ctrl.apiContactEdit.edit(contact);
		}
 
		ctrl.contactEditCanceled = function (){
			ctrl.hideContactEdit();
		}
 
		ctrl.contactUpdated = function (contact){
			ctrl.hideContactEdit();
			ctrl.apiContactList.refreshExistingContact(contact);
		}
 
    }
 
 
    angular.module('myContactApp').component('contactsManagementView', {
        templateUrl: 'components/manageContacts/contactsManagementView.html?' + new Date(),
        controller: ContactsManagementViewController
    });
 
    //END IIFE
})();

Updating the contactService service to provide a method updating contact

Here is the added method to update a contact :

updateContact: function(contact, id){
	return $http.put(url + '/'+id, contact)
			.then(
					function(response){
						return response.data;
					}, 
					function (errResponse) {
						var errorReason = "the contact update has encountered an error from the server side";
						console.error(errorReason);
						return $q.reject(errorReason);
					}
			);
}

We follow still the same way that we used in the previously implemented methods of contactService but this time we call the put() method of the $http service.

Three things to notice:
– like for the deleting method, the id of the contact to update is passed in the url of the request sent to the backend.
– we send the contact object in the request.
– in case of successful update, we return nothing to the caller of the service.

Here is the full updated contactService.js file :

(function () {
    'use strict';
    //IIFE
 
    angular.module('myContactApp')
        .factory('contactService', ContactService);
 
    function ContactService($http, $q) {
 
        var url = 'api/contacts';
 
        return {
            createContact: function (contact) {
                return $http.post(url, contact)
                    .then(
                        function (response) {
                            var newId = MyNgUtil.getIdentifierFromHeaderLocation(response);
                            return newId;
                        },
                        function (errResponse) {
                            var errorReason = "the contact creation has encountered an error from the server side";
                            console.error(errorReason);
                            return $q.reject(errorReason);
                        }
                    );
            },
 
            fetchAllContacts: function () {
                return $http.get(url)
                    .then(
                        function (response) {
                            var contacts = response.data;
 
                            if (contacts == null || contacts.length == 0) {
                                console.info('No contact found');
                            }
                            // convert date in string type to a date type
                            for (var i = 0; i < contacts.length; i++) {
                                if (contacts[i].birthday) {
                                    contacts[i].birthday = new Date(contacts[i].birthday);
                                }
                            }
                            return contacts;
                        },
                        function (errResponse) {
                            var errorReason = "the contacts fetching has encountered an error from the server side";
                            console.error(errorReason);
                            return $q.reject(errorReason);
                        }
                    );
            },
 
			updateContact: function(contact, id){
				return $http.put(url + '/'+id, contact)
						.then(
								function(response){
									return;
								}, 
		                        function (errResponse) {
		                            var errorReason = "the contact update has encountered an error from the server side";
		                            console.error(errorReason);
		                            return $q.reject(errorReason);
		                        }
						);
			},
 
            deleteContact: function (id) {
                return $http.delete(url + '/' + id)
                    .then(
                        function (response) {
                            return;
                        },
                        function (errResponse) {
                            var errorReason = "the contact deletion has encountered an error from the server side";
                            console.error(errorReason);
                            return $q.reject(errorReason);
                        }
                    );
            }
 
        }
    }
 
    //END IIFE
})();

Mocking the PUT HTTP request updating a contact

We will return a mocked response when we request the backend to update a contact.
Here is the added code :

$httpBackend.whenPUT(/api\/contacts\/\d*/)
	.respond(function (method, url, data, headers) {
		console.log("mock put contact with data " + data);
		var contactModified = angular.fromJson(data)
		var index = MyNgUtil.getIndexWithId(contactModified.id, contacts);
		contacts.splice(index, 1, contactModified);
		return [200, {}, undefined];
	});

In order to display updated data to the user when we update a contact, the mocked back end should update the modified contact from the contacts array that simulates back end data.
To do it we need to know the contact id of the updated contact.
In contactService, we have seen that when we send a contact updating request, we provide a contact. We can retrieve the contact id from this object that is provided in the data parameter of the callback of the whenPUT() function :

$httpBackend.whenPUT(/api\/contacts\/\d*/)
.respond(function (method, url, data, headers) {

Here is the full updated version of mockServices.js :

(function () {
    'use strict'; //IIFE
 
 
    var regexMock = new RegExp("/\?.+mock");
    if (!document.URL.match(regexMock)) {
        return;
    }
 
    var myAppDev = angular.module('myContactApp')
 
    .config(function ($provide) {
        $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator);
    })
 
    .run(function ($httpBackend, $http) {
 
        console.info("mocked backend enabled");
 
        // state	   	    	
        var contacts = [
            {
                id: 1,
                firstName: 'John',
                lastName: 'Doe',
                birthday: '',
                email: 'johndoe@secure.com',
                phone: '0134345454',
                sex: 'MALE',
                address: {
                    addressLine: '154 Unknown Street',
                    city: 'Ideal City',
                    zip: '10101',
                    country: 'Wanted Country'
                }
 
            },
 
 
            {
                id: 2,
                firstName: 'Jane',
                lastName: 'Calamity',
                birthday: new Date("1852-05-01"),
                email: 'janecalimity@secure.com',
                phone: '0134345454',
                sex: 'MALE',
                address: {
                    addressLine: '123 Billy The Kid Street',
                    city: 'Western City',
                    zip: '99999',
                    country: 'World Wide West'
                }
 
            }
 
	    			   ];
 
        var nextContactId = contacts.length + 1;
 
        // behavior
        $httpBackend.whenGET(/\.html.*$/).passThrough();
 
        // GET CONTACTS
        $httpBackend.whenGET("api/contacts").respond(contacts);
 
        // POST CONTACT
        $httpBackend.whenPOST("api/contacts")
            .respond(function (method, url, data, headers) {
                console.log("mocked response for POST contact with data " + data);
                var contact = angular.fromJson(data);
                contact.id = nextContactId;
                contacts.push(contact);
                var responseMockHeaders = {
                    Location: 'api/contacts/' + contact.id
                };
                nextContactId++;
                return [201, {}, responseMockHeaders];
        });
 
 
        // PUT CONTACT												  
        $httpBackend.whenPUT(/api\/contacts\/\d*/)
            .respond(function (method, url, data, headers) {
                console.log("mock put contact with data " + data);
                var contactModified = angular.fromJson(data);
                var index = MyNgUtil.getIndexWithId(contactModified.id, contacts);
                contacts.splice(index, 1, contactModified);
                return [200, {}, undefined];
            });
 
        // DELETE CONTACT												  
        $httpBackend.whenDELETE(/api\/contacts\/\d*/)
            .respond(function (method, url, data, headers) {
                console.log("mock delete contact with url " + url);
                var idContact = parseInt(MyNgUtil.getIdentifierFromUrl(url));
                var index = MyNgUtil.getIndexWithId(idContact, contacts);
                if (index != -1) {
                    contacts.splice(index, 1);
                    return [200, {}, undefined];
                }
                return [500, {}, undefined];
            });
 
    }); // run end		
 
 
 
})(); //END IIFE

Updating index.html and app.js

Here is the updated index.html file :

<!DOCTYPE html>
<html ng-app="myContactApp">
 
<head>
    <title>Contacts and Groups Management (Angular JS)</title>
    <style>
 
    </style>
 
    <!-- JQUERY -->
    <script src="lib-js/jquery-2.2.3.js"></script>
 
    <!-- bootstrap JS v3.3.6 -->
    <script src="lib-js/bootstrap.js"></script>
 
    <!-- bootstrap CSS v3.3.6 > -->
    <link rel="stylesheet" type="text/css" href="css/bootstrap.css">
 
    <!-- angular lib -->
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-messages.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-mocks.js"></script>
 
    <!-- Required for angular material -->
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-aria.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular-animate.js"></script>
 
    <!-- angular extension lib -->
    <script src="lib-js/angular-ui-router.js"></script>
    <script src="lib-js/angular-material.js"></script>
    <link rel="stylesheet" type="text/css" href="css/angular-material.css">
 
    <!-- app  -->
    <link rel="stylesheet" type="text/css" href="app.css">
    <script src="app.js"></script>
 
    <!--  native types enhancing and utility functions -->
    <script src="util/my-util.js"></script>
    <script src="util/contact-factory.js"></script>
 
 
    <!--common components-services-->
    <script src="components/common/baseParentComponentController.js"></script>
 
 
    <!-- services -->
    <script src="services/contactService.js"></script>
 
    <!-- mock services -->
    @mockServices@
 
    <!-- contact management components -->
    <script src="components/manageContacts/contactsManagementView.js"></script>
    <script src="components/manageContacts/contactList.js"></script>
    <script src="components/manageContacts/myAddress.js"></script>
    <script src="components/manageContacts/contactEdit.js"></script>
 
    <!-- contact creation components -->
    <script src="components/createContact/contactCreationView.js"></script>
    <script src="components/createContact/contactCreation.js"></script>
 
 
</head>
 
<body>
 
    <div class="container-fluid">
 
        <div class="row">
            <div class="col-md-2 border1PxWithNoPadding">
                <ul class="nav nav-pills nav-stacked">
                    <li ui-sref-active="active"><a ui-sref="manageContacts">Manage existing contacts</a></li>
                    <li ui-sref-active="active"><a ui-sref="createContacts">Create a new contact</a></li>
                </ul>
            </div>
 
            <div class="col-md-10">
                <ui-view></ui-view>
            </div>
        </div>
    </div>
</body>
 
</html>

Here is the updated app.js file :

(function () {
    'use strict';
    //IIFE
 
 
    angular.module('myContactApp', ['ui.router', 'ngMessages', 'ngMaterial', 'ngAnimate', 'ngAria'])
 
    .config(function ($stateProvider, $urlRouterProvider) {
 
        var states = [
            {
                name: 'manageContacts',
                url: '/manageContacts',
                component: 'contactsManagementView'
		},
 
            {
                name: 'createContacts',
                url: '/createContacts',
                component: 'contactCreationView'
		}
 
	];
 
        states.forEach(function (state) {
            $stateProvider.state(state);
        });
 
        $urlRouterProvider.otherwise('/manageContacts');
 
    })
 
 
    // END IIFE
})();

Running the application

As for the previous part, from the command line, under the contact-webapp directory, execute the mvn -Pdev command to include the mock facility to the application.

When the server is started, visit the web page with the mocking backend enabled by browsing  : http://localhost:8095/contact-webapp/#/manageContacts?mock

Downloading the source code

[sdm_download id= »2590″ fancy= »1″]

Next part : Implementing the backend of the application (9/9)

Ce contenu a été publié dans Angular, AngularJS, Bootstrap, java, JavaScript, Spring Boot. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *