The Zurich release has arrived! Interested in new features and functionalities? Click here for more

Paul Kunze
Tera Guru

Recently I had the requirement to create a popup window on the incident form which asks the user additional questions (different ones depending on the Business Service) to enrich the description field with relevant information.

The right way to accomplish this is to create a UI Action on the form that opens a GlideModal which contains a UI Page. So far so good, three simple components that can together fulfill my requirement.

But soon after starting with coding I once again realized how much pain it is to develop Jelly code. UI Pages rely on Jelly and cannot be used without it. Right? I didn’t want to accept this and tried to find a way to use AngularJS instead. It would be so much easier. ServiceNow already ships it for most other purposes so why can’t we also use it in UI Pages?

So together with a colleague I dug into the hidden parts of ServiceNow and after some trial and error we actually managed to get it running. Implementing my new feature was a piece of cake then using the AngularJS functionality. And as it was so helpful for me and definitely will be in the future I want to share with you how we did it.

For demonstration purposes I will here show and describe a popup that will look and act as follows:

find_real_file.png

 

Part 1: The UI Action

First we need our UI Action which enables the user to click a button to open the popup. This step is very short and simple.

For a very basic example I created a UI Action as follows:

  • Name: Show Angular UI Page
  • Table: Incident
  • Action name: open_popup
  • Client: true
  • Form button: true
  • Form style: Primary
  • Onclick: showDialog()
  • Script:
function showDialog(){
    var gm = new GlideModal("angular_ui_page");
    gm.setTitle('This is an AngularJS UI Page');
    gm.render();
}

Nothing special here and just 5 lines of code for the most simple example. Note that you need to instantiate the GlideModal with the name of the UI Page you want to display and set the title to what you want to see in the header of the popup.

find_real_file.png

 

Part 2: The UI Page

If you want to follow my basic example you can fill out the fields as follows:

  • Name: angular_ui_page
  • Category: General
  • HTML:
<html>
    <!-- necessary for pages that do not contain angular yet:
    <sript src="/scripts/angular_includes_no_min.1.5.11.js"></sript>-->
    <!-- HTML Angular code for the popup -->
    <sript id="angular-template" type="text/template">
        /*
        <div ng-controller="angularController">
            Type a color: <input ng-model="name"/>
            <h2 style="color: {{name}}">You entered: {{name}}</h2>
        </div>
        */
    </sript>

    <!-- The HTML tag that will contain the inserted code -->
    <div id="angular-app"></div>
</html>
  • Client script:
var angularApp = angular.module('myApp', []);
angularApp.controller('angularController', function($scope) {
    $scope.name = 'Blue';
});

var appElement = angular.element('#angular-app');
var escapedText = angular.element('#angular-template').text().replace(/^\s*\/\/\s*\/\*\s*|\s*\*\/\s*\/\/\s*$/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
var app = angular.element(escapedText);
angular.bootstrap(app, ['myApp']);
appElement.append(app);

find_real_file.png

 

That is already it!

 

 

And now the explanation of my scripts:

The UI Page HTML

By default every UI Page starts with the following two lines:

<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">

This tells the browser to parse the code as Jelly code and instantiates some variables and namespaces. But as we don’t want to use Jelly we can simply replace this with:

<html>

(remember to also replace </j:jelly> with </html> at the end)

 

When developing a web application with AngularJS you would then normally write

    <div ng-app="myApp" ng-controller="angularController">
        Type a color: <input ng-model="name"/>
        <h2 style="color: {{name}}">You entered: {{name}}</h2>
    </div>
    <script>
        var angularApp = angular.module('myApp', []);
        angularApp.controller('angularController', function($scope) {
            $scope.name = 'Blue';
        });
    </script>

and everything would work. But ServiceNow has its own protection mechanisms and doesn’t allow AngularJS to run in the HTML section of a UI Page. If you look into the developer tools of your browser you will see that the code will not be executed:

find_real_file.png

 

Let’s circumvent these protection mechanisms to run our code where we want it!

First of all we move the script part into the Client script section of the UI Page. There it is allowed to run. But we also need to move the HTML there because the AngularJS expressions (wrapped by curly braces {{}}) need to be interpreted as well.

The most developer friendly way to do this is to write the code in the HTML section (because there we have syntax highlighting) in a script template and then inject it into the actual HTML tag where we want to display the contents. The HTML will then look like this:

<html>
    <!-- HTML Angular code for the popup -->
    <sript id="angular-template" type="text/template">
        <div ng-controller="angularController">
            Type a color: <input ng-model="name"/>
            <h2 style="color: {{name}}">You entered: {{name}}</h2>
        </div>
    </sript>

    <!-- The HTML tag that will contain the inserted code -->
    <div id="angular-app"></div>
</html>

Note: If you are not using the UI Page as a popup on a form then the AngularJS library might not be available yet. In this case you can reference the Angular Script Include at the beginning which is used by ServiceNow in other pages: <sript src="/scripts/angular_includes_no_min.1.5.11.js"></sript>

(also note that I had to write "sript" instead of "script" because the article would otherwise remove the attributes)

 

The UI Page Client Script

Alright, the HTML is written and now we need to breathe life into it by getting AngularJS to run.

As we know, everything an AngularJS application needs is a module and a controller that are bound to an HTML element. This is exactly the part which we moved here from the script tag we initially wanted to use in the HTML section:

var angularApp = angular.module('myApp', []);
angularApp.controller('angularController', function($scope) {
    $scope.name = 'Blue';
});

 

As we need to inject the template into the HTML so that Angular can run we additionally need to fetch that DOM element and bootstrap AngularJS in it before adding it back to the DOM:

var templateText = angular.element('#angular-template').text();
var app = angular.element(templateText);
angular.bootstrap(app, ['myApp']);
var appElement = angular.element('#angular-app');
appElement.append(app);

 

Unfortunately ServiceNow has some protection mechanisms in place here too so at this point the HTML will look as follows in the browser console:

find_real_file.png

All angle brackets (< and >) are escaped and there are also leading and trailing slashes around the code. So what we do is to add our own markers (/*) to the template and replace all these elements back to how they should be using some additional code:

var escapedText = angular.element('#angular-template').text().replace(/^\s*\/\/\s*\/\*\s*|\s*\*\/\s*\/\/\s*$/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">");

 

Now we finally get the result we were looking for:

find_real_file.png

 The code is added correctly and fully working.

Comments
Mayank42
Mega Explorer

Its quite strange why normal Angular pages can run in standalone way, but not as a Modal.

Thanks for the article.

Chandrima Mukhe
Tera Explorer

Really nice article. Though very obvious somebody might overlook there is a typo in the html code `sript`.

Paul Kunze
Tera Guru

Thanks for your feedback 🙂

Yeah unfortunately this page does not let me type "script" as an HTML tag, if I do it the whole content will be deleted. I also mentioned it somewhere in the middle of the article.

Rahman4
Tera Guru

Hello,

Interesting but this doesn't work anymore. I am in Quebec release. Somehow {{}} is not recognized anymore and if I reference the AngularJS from google CDN, I get CORS error. Has anyone been successful with this approach?

 

Sorry, had to mention that I am using it as a Modal using GlideModal ...

 

var dialog = new GlideModal("Sample_AngularJS_Page"); 
dialog.setTitle("Sample AngularJS Popup"); //Set the dialog title
dialog.render(); //Open the dialog

 

 

VM16939:1 Access to XMLHttpRequest at 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js' from origin 'https://myPDI.service-now.com' has been blocked by CORS policy: Request header field x-usertoken is not allowed by Access-Control-Allow-Headers in preflight response.

 

regards,

 

rahman

Paul Kunze
Tera Guru

Hello Rahman,

I just tested it in Quebec and Rome. It still works when following the instructions. You need to pay attention to replace the "sript" tag in the HTML by "script" so that the code gets interpreted.

I also used a GlideModal in a PDI and didn't get any CORS error...
You could check the CORS Rules and try to allow the page there.

Rahman4
Tera Guru

Hi Paul,

I added new CORS rule but its still not working for me.

Can you please post your sample code including the CORS rule, your UI Action code that shows the page as Popup and the sample UI page so we can see what the difference is?

I still get "VM7579:1 Uncaught DOMException: Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js'."

Many thanks

Esteban N
Tera Expert

Do anybody knows if this works for workspace using something like 

 

g_modal.showFrame({
		title: "Take your pick",
		url:
		"/ui_page_ws.do?from_workspace=true",
		size: "xl",
		height: "85vh",
	});

 

In the UI action's section for workspace?

Regards.

_insert name he
Tera Explorer

Anyone have a workaround for the san diego release? Have multiple ui pages based around this example that all broke on an the upgrade.

christophermart
Tera Contributor

I also ran into an issue with using angular in a UI page after the San Diego upgrade and enabling the Polaris theme. I was able to get something working that I thought I would share. I'm hoping the experts will be able to provide a better solution.

 

UI Page HTML: (Removed the angular template)

<html>
<!-- necessary for pages that do not contain angular yet:
<sript src="/scripts/angular_includes_no_min.1.5.11.js"></sript>-->

<!-- The HTML tag that will contain the inserted code -->
<div id="angular-app"></div>
</html>

 

UI Page Client Script: (Added template to script instead. Also added an ng-if to example.)

var angularApp = angular.module('myApp', []);
angularApp.controller('angularController', function($scope) {
$scope.name = 'Blue';
});

var appElement = angular.element('#angular-app');

var html_template = "<div ng-controller=\"angularController\">Type a color: <input ng-model=\"name\"/><div ng-if=\"name=='Blue'\"><h2 style=\"color: {{name}}\">You entered: {{name}}</div></h2></div>";
var escapedText = html_template.replace(/^\s*\/\/\s*\/\*\s*|\s*\*\/\s*\/\/\s*$/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
var app = angular.element(escapedText);
angular.bootstrap(app, ['myApp']);
appElement.append(app);

 

This seems to work on my end, but I'm sure there is a better way of doing this. 

Paul Kunze
Tera Guru

Important note for Instances with the San Diego release or higher:

The script line

var escapedText = angular.element('#angular-template').text().replace(/^\s*\/\/\s*\/\*\s*|\s*\*\/\s*\/\/\s*$/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">");

needs to be replaced with:

var escapedText = angular.element('#angular-template').text().replaceAll('/*', '').replaceAll('*/', '').replaceAll('&lt;', '<').replaceAll('&gt;', '>');

 

(background: ServiceNow changed the way of escaping the code in HTML and does not add // anymore)

BWolfson91
Tera Contributor

Hi @Paul Kunze ,

 

Great article got my modal rendering pretty quick. But having some challenges getting the processing script to run. I have tried a couple different methods to trigger it, such as using a <button type="submit" ng-click="runMyCode"> </button>, <form ng-submit="runMyCode"> <input type="submit"></input></form>. Both methods enter the client side without issues and follow the path to return true. But the processing script isn't picking anything up. Do you have an example where you have submitted something  to process or guidance on how you would approach this? Was trying to stay inside the UI Page for all my code, but it seems I may have to write a glideAjax Script Include.

 

Look forward to hearing your thoughts.

 

Brian

Paul Kunze
Tera Guru

Hi @BWolfson91,

thanks for your feedback.

the processing script only runs when there is a Jelly form which is getting submitted. Submitting an AngularJS form will not trigger the Processing script.

 

So you basically have two options:

  1. Make it a mix of an AngularJS and Jelly page: Additionally import the Jelly namespace with <j:jelly xmlns:g="glide">, then create a <g:ui_form> and add a submit button in it. This should run the Processing script when you click the button.
  2. Use a GlideAjax and call it from a function defined in the AngularJS Scope:
    <button ng-click="submit()">Submit</button> in the HTML and this in the Angular Controller in the Client Script:

 

	$scope.submit = function(){
		console.log('Submit button was clicked'); // or a GlideAjax call
		GlideModal.get().destroy(); // close popup
	};​

 

 

Best regards,
Paul

erck
Tera Contributor

I keep getting

{{name}}

even after typing in the input box

erck
Tera Contributor

How do we pass a value (like record sys id) from the UI Action to the UI page ?

Paul Kunze
Tera Guru

Hi @erck,

you can use the g_form object normally like in any other client script. With that you have access to all variables.

For example if you add this to the HTML:

<div id="number"></div>
<div id="sys_id"></div>

then you can display the number and sys_id of the current record if you add this to the client script:

angular.element('#number').html('Number: ' + g_form.getValue('number'));
angular.element('#sys_id').html('Sys_id: ' + g_form.getUniqueValue());

 Example:

PaulKunze_1-1726494225649.png

 

Version history
Last update:
‎07-24-2020 03:03 AM
Updated by: