Quantcast
Channel: SCN : Blog List - ABAP Development
Viewing all articles
Browse latest Browse all 943

AngularJS Single-Page Application + {z}restapi and token authentication

$
0
0

Building a Single-Page Application (SPA) with AngularJS  and Bootstrap CSS to consume a REST API based on {z}restapi and token authentication

 

In this blog we are going to develop a Single-Page Application (SPA) with AngularJS and Boostrap CSS to store contacts on the SAP WebAS ABAP using a REST API based on {z}restapi and its token based authentication mechanism. The Single-Page Application will allow us to make use of the 4 CRUD methods, Create, Read, Update and Delete, provided by the REST API.



Pre-requisites

 

In order to develop and test the application it is necessary to have:

 

On the Server side

 

  • A SAP WebAS ABAP with the {z}restapi installed ({z}restapi download and installation instructions on GitHub)

 

On the Client side

 

 

Note: you can deploy and run the Single-Page Application (SPA) as a BSP application on your SAP WebAS ABAP or locally using the XAMPP Apache Server. Here we are going to use the XAMPP Apache Server to consume the API from the outside (other domain) in order to explore the Cross Origin Resource Sharing - CORS.

 

 

Downloads

 

You can download below the .nugg files to import the ABAP objects into your system and the zip file with the Single-Page Application.

 

Nugg Files

 

 

Zip File

 

 

GitHub Repository

 

 

 

Dictionary objects

 

If you want to create the dictionary objects manually please find below the details of each object.

 

TABLE


  • ZTB_CONTACTS
    • MANDT         type MANDT
    • EMAIL           type CHAR30
    • FIRSTNAME type CHAR30
    • LASTNAME   type CHAR30
    • PHONE         type CHAR30


TABLE TYPE

 

  • ZTT_CONTACTS line type ZTB_CONTACTS

 

STRUCTURE

 

  • ZST_CONTACTS
    • .INCLUDE ZST_REST_RESPONSE_BASIC
      • SUCCESS  type STRING
      • MSG       type STRING 
      • CODE     type i
    • CONTACTS    type ZTT_CONTACTS

 

 

Implementing the Contacts REST API

 

Let's create the resource class ZCL_REST_RESOURCE_CONTACTS.


Go to SE24 and create the class ZCL_REST_RESOURCE_CONTACTS, final with public instantiation as shown in figure 01.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_01.JPG

    Figure 01 - Class Properties

 

On the interfaces Tab inform the four interfaces provided by {z}restapi

 

  • ZIF_REST_RESOURCE_CREATE    REST API - Resource Create Method
  • ZIF_REST_RESOURCE_READ        REST API - Resource Read Method
  • ZIF_REST_RESOURCE_UPDATE    REST API - Resource Update Method
  • ZIF_REST_RESOURCE_DELETE     REST API - Resource Delete Method

 

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_02.JPG

   Figure 02 - Interfaces Tab

 

Now let's implement the inherited methods. Go to the Methods tab.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_03.JPG

    Figure 03 - Methods Tab

 

First of all let's implement our GET_REQUEST_TYPE and GET_RESPONSE_TYPE methods.

 

The GET_REQUEST_TYPE of all interfaces will have the same request type, what means that all CRUD methods expect to receive the fields of our contacts table as parameters.

 

GET_REQUEST_TYPE method implementation

 

METHOD ZIF_REST_RESOURCE_CREATE~GET_REQUEST_TYPE.  r_request_type = 'ZTB_CONTACTS'.
ENDMETHOD.

 

 

METHOD ZIF_REST_RESOURCE_READ~GET_REQUEST_TYPE.  r_request_type = 'ZTB_CONTACTS'.
ENDMETHOD.

 

 

METHOD ZIF_REST_RESOURCE_READ~GET_REQUEST_TYPE.  r_request_type = 'ZTB_CONTACTS'.
ENDMETHOD.

 

 

METHOD ZIF_REST_RESOURCE_READ~GET_REQUEST_TYPE.  r_request_type = 'ZTB_CONTACTS'.
ENDMETHOD.

 

We will see later (testing the API) that when we call the methods passing parameters with the same name of the contact's table fields the values are automatically passed to the importing structure I_REQUEST, which in our case will have the type ZTB_CONTACTS.

 

The only CRUD method that needs to have the GET_RESPONSE_TYPE method implemented is the READ method, where we will return the contact's data. All other methods don't need to be implemented.


GET_RESPONSE_TYPE method implementation


METHOD ZIF_REST_RESOURCE_READ~GET_RESPONSE_TYPE.

   r_response_type = 'ZST_CONTACTS'.

ENDMETHOD.


We will see later that the {z}restapi has a fall back mechanism that uses the structure ZST_REST_RESPONSE_BASIC when a custom response type is not defined.


Now let's implement the CREATE, READ, UPDATE and DELETE methods.


CREATE method implementation


METHOD zif_rest_resource_create~create.   DATA: ls_contact  TYPE ztb_contacts,         ls_response TYPE zst_rest_response_basic.   ls_contact = i_request.   IF NOT ls_contact IS INITIAL.     INSERT ztb_contacts FROM ls_contact.     IF sy-subrc = 0.       ls_response-success = 'true'.       ls_response-code    = 200.       ls_response-msg     = 'Contact created successfully!'.     ELSE.       ls_response-success = 'false'.       ls_response-code    = 409.       ls_response-msg     = 'Contact already exists!'.     ENDIF.   ELSE.     ls_response-success = 'false'.     ls_response-code    = 403.     ls_response-msg     = 'Contact has no information!'.   ENDIF.   e_response = ls_response.
ENDMETHOD.


 

READ method implementation


METHOD ZIF_REST_RESOURCE_READ~READ.   DATA: ls_request  TYPE ztb_contacts,         ls_response TYPE zst_contacts.   ls_request = i_request.   IF ls_request-email IS INITIAL.     SELECT * FROM ztb_contacts       INTO TABLE ls_response-contacts.   ELSE.     SELECT * FROM ztb_contacts       INTO TABLE ls_response-contacts       WHERE email = ls_request-email.   ENDIF.   e_response = ls_response.
ENDMETHOD.

 

UPDATE method implementation


METHOD zif_rest_resource_update~update.   DATA: ls_contact  TYPE ztb_contacts,         ls_response TYPE zst_rest_response_basic.   ls_contact = i_request.   UPDATE ztb_contacts FROM ls_contact.   IF sy-subrc = 0.     ls_response-success = 'true'.     ls_response-code    = 200.     ls_response-msg     = 'Contact updated successfully!'.   ELSE.     ls_response-success = 'false'.     ls_response-code    = 409.     ls_response-msg     = 'Contact not found!'.   ENDIF.   e_response = ls_response.
ENDMETHOD.


DELETE method implementation


METHOD ZIF_REST_RESOURCE_DELETE~DELETE.   DATA: ls_request  TYPE ztb_contacts,          ls_response TYPE zst_rest_response_basic.   ls_request = i_request.   IF NOT ls_request-email IS INITIAL.     DELETE FROM ztb_contacts       WHERE email = ls_request-email.     IF sy-subrc = 0.       ls_response-success = 'true'.       ls_response-code = 200.       ls_response-msg = 'Contact deleted successfully!'.     ELSE.       ls_response-success = 'false'.       ls_response-code = 409.       ls_response-msg = 'Contact not found!'.     ENDIF.   ENDIF.   e_response = ls_response.
ENDMETHOD.

 

Let's test our Contacts REST Resource using the POSTMAN Google Chrome extension.

 

Assuming that {z}restapi is installed and its ICF service is created under /sap  (see figure 4) the URL of our Contacts service is:

 

http://server:port/sap/zrestapi/myApp/contacts

 

where myApp is the name of our application (that is required by {z}restapi) and contacts is what identifies our contacts resource, i.e., everything that goes after "ZCL_REST_RESOURCE_". As you have already noticed, together they match the resource class name.

 

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_04.JPG

    Figure 4 - {z}restapi ICF service

 

Here we are not going to cover all test cases, only the most basic of the positive test cases.

 

Testing the CREATE method

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_05.JPG

     Figure 5 - testing the create method

 

 

Testing the READ method

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_06.JPG

     Figure 6 - testing the read method

 

 

Testing the UPDATE method

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_07.JPG

     Figure 7 - testing the update method

 

 

Testing the DELETE method

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_08.JPG

    Figure 8 - testing the delete method

 

 

Developing the Contacts AngularJS Web Application

 

Now that our API is ready let's start to develop our frontend application.

 

You can download the zip file with the complete web application from this link.

 

INDEX.HTML page

 

The index.html is a very basic html page where we are going to reference the CSS and javascript files used by the application and do the basic setup of the AngularJS application. Below is a extract of the index.html source code.

 

 

<!DOCTYPE html><html lang="en"><head>  <title>SCN Blog 8 - AngularJS Contacts App with {z}restapi and token authentication</title>  <meta charset="utf-8">  <!-- Mobile Specific Metas -->    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">    <!-- Libs CSS -->    <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" rel="stylesheet">    <link href="http://maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">    <!-- Custom CSS -->    <link href="app.css" rel="stylesheet"></head><body ng-app="myApp">  <!-- Placeholder for the views -->    <div class="container" ng-view=""></div>  <!-- Start Js Files -->    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.6/angular.min.js" type="text/javascript"></script>    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.6/angular-route.min.js" type="text/javascript"></script>    <script src="hmac-sha1.js" type="text/javascript"></script>    <script src="app.js" type="text/javascript"></script></body></html>

On the body tag we are informing the attribute ng-app="myApp" which defines our application and the <div class="container" ng-view=""></div> which is the placeholder for the views of the Single-Page Application.

 

Our Single-Page Application will be composed by 2 views:

 

  • main.html
  • contact.html

 

 

 

MAIN.HTML (view)

 

The main view will have a form to add new contacts and a List to display all contacts.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_09.JPG

    Figure 9 - main view

 

<!-- Overlay to display the loading indicator --><div id="overlay" ng-show="$parent.data.loading"><i id="ajax-loader" class="fa fa-3x fa-spinner fa-spin"></i></div><h3>Add Contact</h3><div class="row" ng-hide="data.showPaneAddContact">  <div class="col-xs-12">  <a ng-click="data.showPaneAddContact=!data.showPaneAddContact"><i class="fa fa-2x fa-plus-square"></i></a>  </div></div><div class="row" ng-show="data.showPaneAddContact">  <div class="col-xs-12">  <a ng-click="data.showPaneAddContact=!data.showPaneAddContact"><i class="fa fa-2x fa-minus-square"></i></a>  </div></div><form ng-show="data.showPaneAddContact" name="contactForm" novalidate class="css-form" role="form" ng-submit="addContact(contactForm)">  <h5 ng-show="$parent.data.message" class="text-center">{{$parent.data.message}}</h5>  <fieldset>  <div class="row">  <div class="form-group col-sm-12 col-sm-3">  <label for="email">Email address</label>  <input type="email" class="form-control" id="email" placeholder="Enter e-mail" ng-model="data.contact.email" ng-maxlength="30" required>  </div>  <div class="form-group col-sm-12 col-sm-3">  <label for="firstname">First Name</label>  <input type="text" class="form-control" id="firstname" placeholder="Enter first name" ng-model="data.contact.firstname" ng-maxlength="30" required>  </div>  <div class="form-group col-sm-12 col-sm-3">  <label for="lastname">Last Name</label>  <input type="text" class="form-control" id="lastname" placeholder="Enter last name" ng-model="data.contact.lastname" ng-maxlength="30" required>  </div>  <div class="form-group col-sm-12 col-md-3">  <label for="phone">Phone</label>  <input type="tel" class="form-control" id="phone" placeholder="Enter phone" ng-model="data.contact.phone" ng-pattern="/^[-+.() ,0-9]+$/" ng-maxlength="30" required>  </div>  </div>  <button type="submit" class="btn btn-primary">Add Contact</button>  <button type="button" class="btn btn-default" ng-click="resetForm(contactForm)">Reset</button>  <button type="button" class="btn btn-default" ng-click="data.showPaneAddContact=!data.showPaneAddContact">Hide</button>  </fieldset></form><h3>Contact List</h3><div class="list-group">  <a href="#/" class="list-group-item" ng-show="isEmpty()">No Contacts</a>    <a href="#/contact/{{contact.email}}" class="list-group-item repeated-item" ng-repeat="contact in data.contacts | orderBy:'+firstname'">        <p><span class="glyphicon glyphicon-user"></span> {{contact.firstname}} {{contact.lastname}}</p>    </a></div>



CONTACT.HTML (view)

 

The contact view will have a form to allow us to update or delete the contact and also call or send e-mail to the contact.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_10.JPG

    Figure 10 - contact view

 

 

<!-- Overlay to display the loading indicator --><div id="overlay" ng-show="$parent.data.loading"><i id="ajax-loader" class="fa fa-3x fa-spinner fa-spin"></i></div><h3>Contact Details</h3><h5 ng-show="$parent.data.message" class="text-center sample-show-hide">{{$parent.data.message}}</h5><form name="contactForm" novalidate class="css-form" role="form">  <fieldset>  <div class="row">  <div class="form-group col-sm-12 col-md-3">  <label for="email">Email address</label>  <input type="email" class="form-control" id="email" ng-model="data.contact.email" readonly>  </div>  <div class="form-group col-sm-12 col-md-3">  <label for="firstname">First Name</label>  <input type="text" class="form-control" id="firstname" placeholder="Enter first name" ng-model="data.contact.firstname" required>  </div>  <div class="form-group col-sm-12 col-md-3">  <label for="lastname">Last Name</label>  <input type="text" class="form-control" id="lastname" placeholder="Enter last name" ng-model="data.contact.lastname" required>  </div>  <div class="form-group col-sm-12 col-md-3">  <label for="phone">Phone</label>  <input type="tel" class="form-control" id="phone" placeholder="Enter phone" ng-model="data.contact.phone" required>  </div>  </div>  <div class="row">  <div class="col-xs-12">  <a href="tel:{{data.contact.phone}}"><i class="fa fa-3x fa-phone-square green"></i></a>  <a href="mailto:{{data.contact.email}}"><i class="fa fa-3x fa-envelope-square green"></i></a>  </div>  </div>  <br/>  <div class="row">  <div class="col-xs-12">  <button type="submit" class="btn btn-primary" ng-click="updateContact(contactForm)">Update</button>  <button type="button" class="btn btn-danger" ng-click="deleteContact()">Delete</button>  <button type="button" class="btn btn-default" ng-click="back()">Back</button>  </div>  </div>  </fieldset></form>

 

 

APP.CSS

 

We need some custom styles to set the border color of our input boxes to red when the field gets the invalid state. We also need some css to style the overlay container that shows a spin icon while the ajax calls is running (waiting the server response).

 

 

.glyphicon-user {  margin-top: 10px;  margin-right: 5px;
}
.css-form input.ng-invalid.ng-touched {    border-color: #FA787E;
}
#overlay{    position: absolute;    top: 0;    left: 0;    width: 100%;    height: 100%;    min-height: 100%;    min-width: 100%;    z-index: 10;    text-align: center;    background-color: rgba(0,0,0,0.5); /*dim the background*/
}
#ajax-loader{    margin-top: 25%;
}
.green{    color: #5cb85c;
}
.green:hover {    color: #449d44;
}

 

 

APP.JS

 

Although it is not a good practice, I decided to keep all javascript code in one file just to keep things as simple as possible. This way I believe that it is easier to understand how the pieces (modules, services, controllers and views) work together to form the application. The drawback here is that having all code in one file makes it a little bit big and can seem to be challenging to understand. But believe me, it will not be that hard.


Before diving into the code let's take a look at the parts or pieces that form the application.

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_11.png

   Figure 11 - App Overview


All application code is encapsulated inside the myApp module. The myApp module has a config where we define the routes and bind Controllers to the Views. It has also two Services, Token Service and Contacts Service and the Controllers Main and Contact.

 


myApp Module


If you have ever tried to use AngularJS to build web applications in the SAP WebAS ABAP you may probably know that the functions $http.post and $http.put provided by the $http service does not behave like jQuery.ajax(). AngularJS transmits data using Content-Type: application/json and the SAP WebAS ABAP is not able to unserialize it. So it is necessary to transform the http request to transmit data using Content-Type: x-www-form-urlencoded. Thanks to Ezekiel Victor we don't need to write the code to do this. The solution is very well explained bt Ezekiel in his blog.


Make AngularJS $http service behave like jQuery.ajax()

http://victorblog.com/2012/12/20/make-angularjs-http-service-behave-like-jquery-ajax/


 

angular.module('myApp', ['ngRoute'], function($httpProvider) {    // Use x-www-form-urlencoded Content-Type    $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';    $httpProvider.defaults.headers.put['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';    /**     * Converts an object to x-www-form-urlencoded serialization.     * @param {Object} obj     * @return {String}     */    var param = function(obj) {        var query = '',            name,            value,            fullSubName,            subName,            subValue,            innerObj,            i;        for (name in obj) {            if (obj.hasOwnProperty(name)) {                value = obj[name];                if (value instanceof Array) {                    for (i = 0; i < value.length; i = i + 1) {                        subValue = value[i];                        fullSubName = name + '[' + i + ']';                        innerObj = {};                        innerObj[fullSubName] = subValue;                        query += param(innerObj) + '&';                    }                } else if (value instanceof Object) {                    for (subName in value) {                        if (value.hasOwnProperty(subName)) {                            subValue = value[subName];                            fullSubName = name + '[' + subName + ']';                            innerObj = {};                            innerObj[fullSubName] = subValue;                            query += param(innerObj) + '&';                        }                    }                } else if (value !== undefined && value !== null) {                    query += encodeURIComponent(name) + '=' + encodeURIComponent(value) + '&';                }            }        }        return query.length ? query.substr(0, query.length - 1) : query;    };    // Override $http service's default transformRequest    $httpProvider.defaults.transformRequest = [        function(data) {            return angular.isObject(data) && String(data) !== '[object File]' ? param(data) : data;        }    ];
});

 

 

 

myApp Config - Defining app routes


The application have only two routes. The "/" points to the main view and "/contact:email/" that points to the contact view.  


/**
* Defines myApp module configuration
*/
angular.module('myApp').config(    /* Defines myApp routes and views */    function($routeProvider) {        $routeProvider.when('/', {            controller: 'MainController',            templateUrl: 'main.html'        });        $routeProvider.when('/contact/:email/', {            controller: 'ContactController',            templateUrl: 'contact.html'        });        $routeProvider.otherwise({            redirectTo: '/'        });    }
);





Token Service


The Token Service is used by the Contacts Service to create authentication tokens for each http request sent to the server. It concatenates all parameters, the private key and a timestamp separated by pipes "|" and creates a hash (SHA1). It also returns the auth_token_s2s that contains the string to sign that is the name (and order) of the parameters that the server must use to calculate the hash and verify the token. The auth_token_uid contains the User Id. Notice that the Private Key is not sent along with the http request. The server already knows it. The askForPkey method prompts the user to inform it so the service can use it to create the authentication tokens.



/**
*  Creates the token service, responsible for generating the authentication tokens
*/
angular.module('myApp').service('TokenService', function($rootScope) {    // Stores user's private key used to create the token in the getToken method.    this.pkey = '';    var self = this;    this.askForPkey = function() {        self.pkey = window.prompt("Please inform your private key", "12345");    };    this.setPkey = function(sPkey) {        self.pkey = sPkey;    };    this.getToken = function(params) {        var timestamp = Date.now();        var auth_token_con = '';        var auth_token_s2s = '';        angular.forEach(params, function(value, key) {            auth_token_con = auth_token_con + value + '|';            auth_token_s2s = auth_token_s2s + key + '|';        });        if (self.pkey === '') {            self.askForPkey();        }        auth_token_con = auth_token_con + timestamp + '|' + self.pkey;        auth_token_s2s = auth_token_s2s + 'timestamp';        var token = CryptoJS.SHA1(auth_token_con);        var auth = {            token: token.toString(),            token_s2s: auth_token_s2s,            token_uid: 'IU_TEST',            timestamp: timestamp        };        return auth;    };
});





Contacts Service


The contacts service is the heart of the application. All communication with the server is handled by this service at it is also responsible for storing the contacts data displayed by the application views.


It has basically the implementation of the 4 CRUD methods that allow the application to Create, Read, Update and Delete contacts on the server side through the Contacts REST API. It makes use of the token service to create the authentication token to sign every single http request sent to the server.



/**
*  Creates the contacts service, responsible for storing and handling contact's data
*/
angular.module('myApp').service('ContactsService', function($http, $location, $rootScope, TokenService) {    this.contacts = [];    this.contact = {        email: '',        firstname: '',        lastname: '',        phone: ''    };    this.rootScope = $rootScope;    this.isFinished = true;    this.serverUrl = 'http://yourserver:port/sap/zrestapi/myApp/contacts';    var self = this;    this.loadingFinished = function() {        window.setTimeout(function() {            self.rootScope.$apply(function() {                self.rootScope.data.loading = false;            });        }, 500);    };    /**     *  Adds a new contact     */    this.addContact = function(contact) {        var auth = TokenService.getToken(contact);        var params = {            email: contact.email,            firstname: contact.firstname,            lastname: contact.lastname,            phone: contact.phone,            timestamp: auth.timestamp        };        self.rootScope.data.loading = true;        $http.post(self.serverUrl,            params,            {                timeout: 30000,                headers: {                    'Auth-Token': auth.token,                    'Auth-Token-S2S': auth.token_s2s,                    'Auth-Token-UID': auth.token_uid                }            }        ).success(function(oData) {            self.loadingFinished();            if (oData.success === 'true' || oData.success === true) {                self.contacts.push(contact);                     self.contact.email = '';                self.contact.firstname = '';                self.contact.lastname = '';                self.contact.phone = '';                if (angular.isString(oData.msg)) {                    self.rootScope.data.message = oData.msg;                }            }else{                if (angular.isString(oData.msg)) {                    self.rootScope.data.message = oData.msg;                }            }        }).error(function(data, status) {            self.loadingFinished();            if (status === 0) {                self.rootScope.data.message = 'Communication error: timeout.';            }else if(data !== null && data !== undefined ) {                    if (data.msg !== undefined) {                               if (angular.isString(data.msg)) {                            self.rootScope.data.message = data.msg;                        }else{                            self.rootScope.data.message = 'Communication error: server returned code ' + status;                        }                    }else{                        self.rootScope.data.message = 'Communication error: server returned code ' + status;                    }            }else{                self.rootScope.data.message = 'Communication error: server returned code ' + status;            }        });    };    /**     *  Retrieves all contacts     */    this.getContacts = function() {        var auth = TokenService.getToken({});        self.rootScope.data.loading = true;        $http.get(self.serverUrl, {            timeout: 30000,            params: {                timestamp: auth.timestamp            },            headers: {                'Auth-Token': auth.token,                'Auth-Token-S2S': auth.token_s2s,                'Auth-Token-UID': auth.token_uid            }        }).success(function(oData) {            self.loadingFinished();            if (angular.isArray(oData.contacts)) {                         self.contacts.splice(0, self.contacts.length);                         oData.contacts.forEach(function (contact) {                    self.contacts.push(contact);                });            }        }).error(function(data, status) {            self.loadingFinished();            if (status === 0) {                self.rootScope.data.message = 'Communication error: timeout.';            }else if(data !== null && data !== undefined ) {                    if (data.msg !== undefined) {                               if (angular.isString(data.msg)) {                            self.rootScope.data.message = data.msg;                        }else{                            self.rootScope.data.message = 'Communication error: server returned code ' + status;                        }                    }else{                        self.rootScope.data.message = 'Communication error: server returned code ' + status;                    }            }else{                self.rootScope.data.message = 'Communication error: server returned code ' + status;            }        });    };    /**     *  Retrieves contact's data     *  @param {Object} obj     */    this.getContact = function(id) {        var email = id;        var auth = TokenService.getToken({email:id});        self.rootScope.data.loading = true;        $http.get(self.serverUrl, {            timeout: 30000,            params: {                email: email,                timestamp: auth.timestamp            },            headers: {                'Auth-Token': auth.token,                'Auth-Token-S2S': auth.token_s2s,                'Auth-Token-UID': auth.token_uid            }        }).success(function(oData) {            self.loadingFinished();            if (angular.isArray(oData.contacts)) {                oData.contacts.forEach(function (contact) {                                 self.contact.email = contact.email;                    self.contact.firstname = contact.firstname;                    self.contact.lastname = contact.lastname;                    self.contact.phone = contact.phone;                });            }        }).error(function(data, status) {            self.loadingFinished();            if (status === 0) {                self.rootScope.data.message = 'Communication error: timeout.';            }else if(data !== null && data !== undefined ) {                    if (data.msg !== undefined) {                               if (angular.isString(data.msg)) {                            self.rootScope.data.message = data.msg;                        }else{                            self.rootScope.data.message = 'Communication error: server returned code ' + status;                        }                    }else{                        self.rootScope.data.message = 'Communication error: server returned code ' + status;                    }            }else{                self.rootScope.data.message = 'Communication error: server returned code ' + status;            }        });    };    /**     *  Updates selected contact     */    this.updateContact = function() {        var auth = TokenService.getToken(self.contact);        var params = {            email: self.contact.email,            firstname: self.contact.firstname,            lastname: self.contact.lastname,            phone: self.contact.phone,            timestamp: auth.timestamp        };        self.rootScope.data.loading = true;        $http.put(self.serverUrl,            params,            {                timeout: 30000,                headers: {                    'Auth-Token': auth.token,                    'Auth-Token-S2S': auth.token_s2s,                    'Auth-Token-UID': auth.token_uid                }            }        ).success(function(oData) {            self.loadingFinished();            if (oData.success === 'true' || oData.success === true) {                if (angular.isString(oData.msg)) {                    self.rootScope.data.message = oData.msg;                }            }else{                if (angular.isString(oData.msg)) {                    self.rootScope.data.message = oData.msg;                }            }        }).error(function(data, status) {            self.loadingFinished();            if (status === 0) {                self.rootScope.data.message = 'Communication error: timeout.';            }else if(data !== null && data !== undefined ) {                    if (data.msg !== undefined) {                               if (angular.isString(data.msg)) {                            self.rootScope.data.message = data.msg;                        }else{                            self.rootScope.data.message = 'Communication error: server returned code ' + status;                        }                    }else{                        self.rootScope.data.message = 'Communication error: server returned code ' + status;                    }            }else{                self.rootScope.data.message = 'Communication error: server returned code ' + status;            }        });    };    /**     *  Deletes selected contact     */    this.deleteContact = function() {        var auth = TokenService.getToken({email: self.contact.email});        self.rootScope.data.isFinished = false;        self.rootScope.data.loading = true;        $http.delete(self.serverUrl, {            timeout: 30000,            params: {                email: self.contact.email,                timestamp: auth.timestamp            },            headers: {                'Auth-Token': auth.token,                'Auth-Token-S2S': auth.token_s2s,                'Auth-Token-UID': auth.token_uid            }        }).success(function(oData) {            self.loadingFinished();            if (oData.success === 'true' || oData.success === true) {                if (angular.isString(oData.msg)) {                    self.rootScope.data.message = oData.msg;                                 self.contact.email = '';                    self.contact.firstname = '';                    self.contact.lastname = '';                    self.contact.phone = '';                    self.rootScope.data.isFinished = true;                }            }else{                if (angular.isString(oData.msg)) {                    self.rootScope.data.message = oData.msg;                }            }        }).error(function(data, status) {            self.loadingFinished();            if (status === 0) {                self.rootScope.data.message = 'Communication error: timeout.';            }else if(data !== null && data !== undefined ) {                    if (data.msg !== undefined) {                               if (angular.isString(data.msg)) {                            self.rootScope.data.message = data.msg;                        }else{                            self.rootScope.data.message = 'Communication error: server returned code ' + status;                        }                    }else{                        self.rootScope.data.message = 'Communication error: server returned code ' + status;                    }            }else{                self.rootScope.data.message = 'Communication error: server returned code ' + status;            }        });    };    /**     * Reset selected contact in order to clear the form     */    this.resetSelectedContact = function(id) {        self.contact.email = '';        self.contact.firstname = '';        self.contact.lastname = '';        self.contact.phone = '';    };
});




Main Controller


The main controller make use of the Contacts Service to read all contacts (to display them on the contacts list) and to Add new contacts. It is important to highlight here that the $scope.data.contacts points to the ContactsService.contacts, so whenever the ContactsService updates its contacts data the view is automatically updated thanks to AngularJS data-binding.



/**
*  Defines the main controller (for the view main.html)
*/
angular.module('myApp').controller('MainController',    function($scope, $rootScope, ContactsService) {        var rootScope = $rootScope;        $scope.data = {};        if ($rootScope.data === undefined) {            $rootScope.data = {};            $rootScope.data.message = null;            $rootScope.data.pkey = null;        }        $rootScope.data.loading = false;        ContactsService.resetSelectedContact();        $scope.data.contact = ContactsService.contact;        $scope.data.contacts = ContactsService.contacts;        $scope.data.showPaneAddContact = false;        ContactsService.getContacts();        /**         *  Checks whether the contacts array is empty or not         */        $scope.isEmpty = function(){            if($scope.data.contacts.length > 0){                return false;            }else{                return true;            }        };        /**         *  Adds a new contact         */        $scope.addContact = function(form){            if(form.$valid === true) {                   var contact = {                    email: $scope.data.contact.email,                    firstname: $scope.data.contact.firstname,                    lastname: $scope.data.contact.lastname,                    phone: $scope.data.contact.phone                };                ContactsService.addContact(contact);                form.$setUntouched();            }else{                $rootScope.data.message = "All fields are required!";            }        };        $scope.resetForm = function(form){            ContactsService.resetSelectedContact();            form.$setUntouched();        };        /**         *  Clears displayed messages after 3 seconds         */        $scope.resetMessage = function() {            window.setTimeout(function() {                rootScope.$apply(function() {                    rootScope.data.message = null;                });            }, 3000);           };        /**         *  Watches changes of the variable "$rootScope.data.message" in order to clear         *  displayed messages after 3 seconds         */        $rootScope.$watch(function(scope) { return scope.data.message },            function(newValue, oldValue) {                if (newValue !== oldValue && newValue !== "") {                    $scope.resetMessage();                };            }        );    }
);




Contact Controller


The Contact Controller also make use of the ContactService to read the selected contact's data, update the selected contact or delete it. Just like the main controller, the $scope.data.contact points to the ContactsService.contact.


 

/**
*  Defines the contact controller (for the view contact.html)
*/
angular.module('myApp').controller('ContactController',    function($scope, $routeParams, $location, $rootScope, ContactsService) {        var rootScope = $rootScope;        $rootScope.data = {};        $rootScope.data.message = null;        $rootScope.data.isFinished = true;        $scope.data = {};        $scope.data.isFinished = $rootScope.data.isFinished;        $scope.data.contact = ContactsService.contact;        ContactsService.getContact($routeParams.email);        /**         *  Executes the updateContact method of ContactsService to update the selected contact.         *  No information needs to be passed to identify the selected contact because the service knows who it is.         */        $scope.updateContact = function(form){            if(form.$valid === true) {                ContactsService.updateContact();            }else{                $rootScope.data.message = "All fields are required!";            }        };        /**         *  Executes the deleteContact method of ContactsService to delete the selected contact.         *  No information needs to be passed to identify the selected contact because the service knows who it is.         */        $scope.deleteContact = function(){            ContactsService.deleteContact();        };        /**         *  Navigates back to the main view         */        $scope.back = function(){            ContactsService.resetSelectedContact();            $location.url("/");        };        /**         *  Clears displayed messages after 3 seconds         */        $scope.resetMessage = function() {            window.setTimeout(function() {                rootScope.$apply(function() {                    rootScope.data.message = null;                });            }, 3000);           };        /**         *  Watches changes of the variable "$rootScope.data.isFinished" in order to trigger         *  the navigation back to the main view when a contact is deleted         */        $rootScope.$watch(function(scope) { return scope.data.isFinished },            function(newValue, oldValue) {                if (newValue === true && oldValue === false) {                    $scope.back();                };            }        );        /**         *  Watches changes of the variable "$rootScope.data.message" in order to clear         *  displayed messages after 3 seconds         */        $rootScope.$watch(function(scope) { return scope.data.message },            function(newValue, oldValue) {                if (newValue !== oldValue && newValue !== "") {                    $scope.resetMessage();                };            }        );    }
);



Testing the application locally with XAMPP



http://www.jianelli.com.br/scnblog8/figures/scnblog8_12.png

   Figure 12 - Starting with no contacts


http://www.jianelli.com.br/scnblog8/figures/scnblog8_13.JPG

   Figure 13 - Adding a new contact


http://www.jianelli.com.br/scnblog8/figures/scnblog8_14.JPG

   Figure 14 - Contact Added


http://www.jianelli.com.br/scnblog8/figures/scnblog8_15.JPG

http://www.jianelli.com.br/scnblog8/figures/scnblog8_16.JPG

   Figure 15 and 16- Updating the contact


 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_17.JPG

   Figure 17 - Contact updated!



http://www.jianelli.com.br/scnblog8/figures/scnblog8_18.JPG

   Figure 18 - Contact list updated


http://www.jianelli.com.br/scnblog8/figures/scnblog8_19.JPG

   Figure 19 - Contact list empty again after deleting the contact

 

http://www.jianelli.com.br/scnblog8/figures/scnblog8_20.JPG

   Figure 20 - No contacts


http://www.jianelli.com.br/scnblog8/figures/scnblog8_21.JPG

   Figure 21 - Authentication token sent in the request header



You can see a live demo at


SCN Blog 8 - AngularJS Contacts App with {z}restapi and token authentication


but this demo uses a PHP backend to simulate the SAP WebAS responses.


All code is available on GitHub


christianjianelli/scnblog8 · GitHub





Viewing all articles
Browse latest Browse all 943

Trending Articles