In this particular tutorial, we are going to build upon the use cases in our previous AngularJS tutorial. We will continue to leverage the REST API but will be introducing the concept of “Promises” in AngularJS.
Please note, as a prerequisite, you should check out my prior blog posts:
- Getting AngularJS Libraries into your ServiceNow instance
- Angular in ServiceNow: Tutorial #1 – Model Basics
- Angular in ServiceNow: Tutorial #2 – Controllers
- Angular in ServiceNow: Tutorial #3 – REST Calls
Considerations
This example will leverage the official ServiceNow REST API that was released with Eureka earlier in 2014. If you wish to do something similar with a pre-Eureka build, I recommend you try the JSONv2 web service.
Due to browser security constraints you will typically only be able to make REST web service calls against the same instance that the javascript is running on.
What is a Promise?
Web service calls in AngularJS are inherently asynchronous. This means that in a script, you may fire off a web service call. However, the next line of your script after that call cannot assume that there is a response waiting to be leveraged. The response will come separately from the function that called it.
An AngularJS Promise is a method that Angular uses to allow you to make a web service call, but establish a promise that when it gets a response back, it will continue with a certain bit of code.
There is a great example of a promise at the following allegorical blog post: Promises in AngularJS, Explained as a Cartoon
Use Case
In this use case we will build upon the use case in the previous AngularJS tutorial. In addition to the input box that filters a list of Incident record numbers, we will add a “Short Description” text area field. This field will display the contents of the resulting incident record when a valid number of fully typed out in the Incident Number input field. We will also add a couple of buttons. The first will be an “Update” button. If the Short Description is changed and the Update button is clicked, then we will post an Update to ServiceNow through AngularJS. The second button will open up the incident record itself so that we can verify that our short description was updated.
We will start with the following basic Jelly/HTML code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?xml version="1.0" encoding="utf-8" ?> <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null"> <g:requires name="angularjs.min.1.3.2.jsdbx" /> <g2:evaluate var="jvar_stamp"> var gr = new GlideRecord('sys_ui_script'); gr.get("6c8b22875ba8310060889508ee4254d4"); gr.getValue('sys_updated_on'); </g2:evaluate> <g:requires name="ang_lesson_4_lib.jsdbx" params="cache=$[jvar_stamp]" /> <div ng-app="descEditor" style='padding:40px;'> <div ng-controller="DescriptionText"> <table><tr> <td style='width: 300px; vertical-align: top;'> <p>NUMBER</p><input ng-model="incnum" ng-change="updateRecordList(incnum);"/><br/> <p>SHORT_DESCRIPTION</p> <textarea style="width: 300px;"/><br/> <button >Update</button> <button >View Record</button> <p>{{status}}</p> </td> <td ng-model="inclist" style='vertical-align: top; padding: 20px; background-color: #B2B2B2; width: 200px;'> <div ng-repeat="num in numbers">{{num.number}}</div> </td> </tr></table> </div> </div> </j:jelly> |
This code produces a form that looks like this:
Set up the initial AngularJS Script
We are going to start off with code from our last tutorial and build upon that for our Angular script. Please note that I am doing this in a separate UI Script in my ServiceNow instance. I have already referenced this script in the Jelly/HTML code above.
Here is the script we will start with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | var myApp = angular.module('descEditor', []); myApp.controller('DescriptionText', ['$scope', '$http', function($scope, $http) { $scope.incnum = "INC0010005"; $scope.url = '/api/now/table/incident'; $http.defaults.headers.common.Accept = "application/json"; $scope.updateRecordList = function(incnum) { $http({ method: 'GET', url: $scope.url + "?sysparm_query=numberSTARTSWITH"+incnum+"^ORDERBYDESCnumber&sysparm_fields=number&sysparm_limit=25", headers: {'Content-Type': 'application/json'} }). success( function(data, status) { $scope.numbers = data.result; }). error ( function(data, status) { $scope.numbers = [{"number": "Error fetching list"}]; }); } }; }] ); |
Build out the Angular Hooks in the HTML
First, lets edit the Incident Number input field action. Right now we have it updating the list of matching records whenever that field changes. However, in addition to that, we want to populate the Short Description text area field as well. To do this we will add an additional function to the “ng-change” directive on the input field and have it call a “getRecordDescription” function.
Now, we want to set up a model on our Text Area field. Since it is going to contain the “short description” of the incident record, let’s tie an “incdesc” variable to the text area. That way, as changes are made to that field, they are captured into a variable.
Also, we are going to have a status label on the form for when we post updates to the short description. If the short description text is changed in the text area it will need another update so, we will want to clear the last success or failure message to indicate that this new short description doesn’t have a status yet. To do this, we will use an “ng-change” directive on the text area field. When the value changes, we will clear out any possible status that may have already been set. We will use a “clearStatus” function that we will build out later.
For our two buttons, we want to add some actions that are triggered when the buttons are pushed. For the Update button we will call a “saveDesc” function that we will develop later. For the “View Record” button, we will call a “gotoRecord()” function that we will set up soon.
This is what our final Jelly/HTML code looks like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?xml version="1.0" encoding="utf-8" ?> <j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null"> <g:requires name="angularjs.min.1.3.2.jsdbx" /> <g2:evaluate var="jvar_stamp"> var gr = new GlideRecord('sys_ui_script'); gr.get("6c8b22875ba8310060889508ee4254d4"); gr.getValue('sys_updated_on'); </g2:evaluate> <g:requires name="ang_lesson_4_lib.jsdbx" params="cache=$[jvar_stamp]" /> <div ng-app="descEditor" style='padding:40px;'> <div ng-controller="DescriptionText"> <table><tr> <td style='width: 300px;'> <p>NUMBER</p><input ng-model="incnum" ng-change="getRecordDescription(incnum); updateRecordList(incnum);"/><br/> <p>SHORT_DESCRIPTION</p> <textarea ng-model='incdesc' ng-change="clearStatus()" style="width: 300px;"/><br/> <button ng-click="saveDesc(incdesc)">Update</button> <button ng-click="gotoRecord()">View Record</button> <p>{{status}}</p> </td> <td ng-model="inclist" style='vertical-align: top; padding: 20px; background-color: #B2B2B2; width: 200px;'> <div ng-repeat="num in numbers">{{num.number}}</div> </td> </tr></table> </div> </div> </j:jelly> |
Create the Factory Angular Code the implements the Promise
First, let’s cover the need to introduce the “Promise” concept in our use case. With the ServiceNow REST API, you can get a single record once you know the SYS_ID of that record. The problem with our example is that we only know the number. Now, we could have had our incident number query also return corresponding short description fields that we could reference in our incident list, but what fun would that be.
What we are going to do instead is make a web service call that uses the REST API to submit a full incident number into a LIST query. Then we will pull the sys_id from that result. Once we get the sys_id in the response, we will make a second call to ServiceNow to get all the data on that record for that sys_id.
Since AngularJS uses asynchronous web services, we have to establish a promise around getting the sys_id for an incident number. Once we have the sys_id (the promise is fulfilled), then we turn around and use that sys_id to get all the incident’s record details.
In order to set up a promise, we have to create a “Factory” on our Angular app. I am going to name my factory “IncidentTableService”:
The primary function in this factory is a getSysID function that takes in an incident number and promises to return the corresponding sys_id. Now, the factory will not have the Controller’s scope variables such as $scope.url, so we will require that the url be passed into the function with the incident number:
Now, calling the REST api in a Promise situation is quite similar to how we did it in our previous tutorial within the controller. However, instead of just calling the $http provider, we are going to “return” it within our function. The following code shows that we first set up the URL to fit the API specifications, and then we set up the GET request:
Aren’t quite done yet, though. Remember, when making REST requests through the controller, we used a .success and .error function attached to the $http provider call? Well a promise is handled a bit differently. For a promise, we add a “.then” call attached to the $http call.
In the “then” situation, a Result variable (we will establish it as “res”) will contain the data from the response. The ServiceNow API response will have a list of “results”. We will pull the first result since numbers are supposed to be unique. Once we have the first result, we want to return the sys_id value on that record.
In the following code, I do a check on the various parts of what I expect from the response. Then I return the sys_id from the response parts:
Now that we have our factory built, we will need to reference it in our Controller that we have already started. To do this, you enter in the name of the factory when you pass in the $scope and $http provides into the controller.
Set up the rest of the Angular Code
Now let’s get back into more familiar territory. Within our Controller’s constructor, let initialize a couple of variables. First, let’s set a sys_id variable to an empty string and then let’s also set up a “status” variable where we eventually can post REST status messages for record updates.
We will now build out our supporting functions that we stubbed out in the HTML for the controller.
First, let’s build a simple controller function that clears the status message as needed:
Next, let’s build out a function in our controller that saves a new short description:
We will also set up the function that redirects us to the actual servicenow record so when they push the “View Record” button our browser will redirect to the ServiceNow stock UI for incidents:
Now, our final controller function is the one that gets the description of the record. We will go into this one in more detail since we are going to leverage that Promise function that we created in our factory. This function is first going to establish the promise to our “getSysID” function from the factory we created:
As you may recall, the promise function was returning the sys_id string. Thus the function within the promise is set up to have a sys_id variable passed into it. We will take that sys_id variable and store it within the controller scope. Then we will issue the GET command on the Incident Table API and submit the sys_id to the Table URL to get the description for the that specific record.
Now for our final step, we want to get the description on the page load, should there be a valid Incident Number set by default. To do this, we call our newly created getRecordDescription function from the bottom of the controller definition:
Here is our final Javascript code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | var myApp = angular.module('descEditor', []); myApp.factory('IncidentTableService', function($http) { return { getSysID: function(num, url){ var reqUrl = url + "?sysparm_query=number="+num; return $http({ method: 'GET', url: reqUrl, headers: {'Content-Type': 'application/json'} }). then( function(res) { if( !res || !res.data || !res.data.result || res.data.result.length == 0 ){ return; } return res.data.result[0].sys_id; }); } } }); myApp.controller('DescriptionText', ['$scope', '$http', 'IncidentTableService', function($scope, $http, IncidentTableService) { $scope.status = ""; $scope.incnum = "INC0010005"; $scope.url = '/api/now/table/incident'; $scope.sys_id = ''; $http.defaults.headers.common.Accept = "application/json"; $scope.clearStatus = function() { $scope.status = ""; }; $scope.getRecordDescription = function(num) { $scope.incdesc = ""; var promise = IncidentTableService.getSysID(num, $scope.url); promise.then( function(sys_id) { $scope.incid = sys_id; $http({ method: 'GET', url: $scope.url+"/"+sys_id, headers: {'Content-Type': 'application/json'} }). success( function(data, status) { if( !data || !data.result ){ return; } $scope.incdesc = data.result.short_description; }); } ); }; $scope.saveDesc = function(desc) { $http({ method: 'PUT', url: $scope.url + "/" + $scope.incid, headers: {'Content-Type': 'application/json'}, data: {'short_description': desc} }). success( function(data, status){ $scope.status = "Successfully Updated"; } ). error( function(data, status){ $scope.status = "ERROR Updating Record: " + data.error.message +": "+ data.error.detail; } ) } $scope.gotoRecord = function() { window.location="/incident.do?sys_id="+$scope.incid; } $scope.updateRecordList = function(incnum) { $http({ method: 'GET', url: $scope.url + "?sysparm_query=numberSTARTSWITH"+incnum+"^ORDERBYDESCnumber&sysparm_fields=number&sysparm_limit=25", headers: {'Content-Type': 'application/json'} }). success( function(data, status) { $scope.numbers = data.result; }). error ( function(data, status) { $scope.numbers = [{"number": "Error fetching list"}]; }); } $scope.getRecordDescription($scope.incnum); }] ); |