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
- AngularJS
- Bootstrap CSS
- CryptoJS (SHA1 hash function used to generate the tokens)
- XAMPP Apache Server (optional)
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
- .INCLUDE ZST_REST_RESPONSE_BASIC
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.
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
Figure 02 - Interfaces Tab
Now let's implement the inherited methods. Go to the Methods tab.
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:
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.
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
Figure 5 - testing the create method
Testing the READ method
Figure 6 - testing the read method
Testing the UPDATE method
Figure 7 - testing the update method
Testing the DELETE method
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.
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.
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.
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
Figure 12 - Starting with no contacts
Figure 13 - Adding a new contact
Figure 14 - Contact Added
Figure 15 and 16- Updating the contact
Figure 17 - Contact updated!
Figure 18 - Contact list updated
Figure 19 - Contact list empty again after deleting the contact
Figure 20 - No contacts
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