Help
cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
Markus Kraus
Kilo Sage

EDIT: 26 June 2023: The way presented in this post is now outdated. I have created a scoped application which enables Client Side UI Actions on the Form Widget and the Data Table Widget (support for row buttons, footer buttons and header buttons). Additionally form annotations are enabled and even data table field styles. Check out my new article:
https://www.servicenow.com/community/developer-articles/show-case-the-service-portal-experience-app-...

 

Overview

  1. Why Client Side UI Actions
  2. How this method works and its advantages
  3. Client Side Button Widget (and how to add it to a Portal Page)
  4. Final thoughts


Why Client Side UI Actions

  • Save without mandatory (= Save as draft)
  • Confirm on deletes (Confirm Popup)
    find_real_file.png

How this method works and its advantages

  • Instead of modifying the OOTB Form Widget, a sibling widget is added which injects all client side code
  • No Cloning Required: Form Widget is not changed
  • Upgrade Safety: Form Widget is not changed
  • No Change to UI Action configuration required
  • Client Side Code is strictly separated from Server Side Code
  • The Widget which enables the Client Side Buttons has less than 40 lines of code
  • The method works both in global scope and as well as in a scoped app

To enable Client Side UI Actions, the following has to be considered:

  • The UI Actions that are currently visible on the Portal remain unchanged (this means that they still have to be marked as "Client: false" and "Form button: true")
  • A new widget must be created, this widget must be a sibling of Form Instance on the Page that displays the form.
    The steps to do that are shown below.

Client Side Buttons Widget (and how to add it to a Portal Page)

  1. Create a new widget in the scope of your choice (global or scoped - it doesn't matter)
  2. Fill in the following fields:
    Name: Client Side Buttons
    Client Controller:
    api.controller=function() {
    	/* widget controller */
    	var c = this;
    	c.buttonClicked = function (action, g_form) {		
    		if (action.action_name.includes('delete')) {
    			return confirm('Are you sure you want to delete this record?');
    		} else if (action.sys_id == '<sys_id_of_my_ui_action') {
    			// do stuff like spModal.open
    		}
    	};
    };

    Link:

    function link(scope, element, attrs, controller) {
    	var formWidgetSysId = 'fd1f4ec347730200ba13a5554ee490c0';
    	var formElement = element
    		.parent()
    		.closest('div') // column
    		.find('.v' + formWidgetSysId);
    
    	if (formElement.length === 0) {
    		// form widget not yet loaded, try again in 50ms
    		setTimeout(link.apply.bind(link, null, arguments), 50);
    		return;
    	}
    
    	// save the OOTB triggerUIAction and replace it with our own (that calls client side code)
    	var formScope = formElement.scope();
    	var ootbTriggerUIAction = formScope.triggerUIAction;
    	formScope.triggerUIAction = function (action) {
    		var g_form = getGForm(formElement);		
    		var result = controller.buttonClicked(action, g_form);
    		if (result || result === undefined) {
    			return ootbTriggerUIAction(action);
    		}
    	};
    
    	function getGForm(formElement) {
    		return formElement
    			.find('div[form_model]') // this contains the actual form (sp-model)
    			.children()
    			.last() // execItemScripts() div which contains the getGlideForm-function
    			.scope()
    			.getGlideForm();
    	}
    }
  3. Save the Form

Enable the Buttons on Service Portal Form Page:

  1. Open the Service Portal Page Record (for the OOTB form: sp_page.list -> filter for id=form)
  2. Click the Column that contains the Form Widget and copy the sys_id of the Column (sp_column) Record
  3. Enter sp_instance.list in the filter navigator and create a new Instance (sp_instance) Record
  4. Enter the following Data:
    Widget: Client Side Buttons
    Column: Open the magnifying lens and apply the filter: [Sys ID] ["starts with" or "is"] [sys_id_from_step2]
    find_real_file.png
    Order: 2
  5. Save the Record

The Page Record should now look like this:
find_real_file.png

Final thoughts

The Client Controller is prepared with an example that shows you how to add code, GlideAjax is also supported, however if you want to add asynchronous calls, you have to do a few modifications to the Widget (hint: use $q).
If you want the whole "Batteries included"-Package, please take a look at the "Swiss Tool" in the official ServiceNow App Store, which includes (just a small extract):

  • a more advanced version of the client side portal buttons
  • related *and* embedded lists for the ootb form widget
  • mandatory attachment handling
  • UI Policy based button visible/disable states
Comments
ITSM Dev
Mega Expert

Thank you for this write-up!

Have a question about this part

The Client Controller is prepared with an example that shows you how to add code, GlideAjax is also supported, however if you want to add asynchronous calls, you have to do a few modifications to the Widget (hint: use $q).

I would have thought that GlideAjax is not possible in a widget due to this KB article.

Would you mind providing a short example of how to include GlideAjax in the Client Controller? I know how GlideAjax looks like otherwise so an example of the server-side Script Include is not needed.

Markus Kraus
Kilo Sage

GlideAjax calls to scoped script includes are affected by this, the workaround is quite simple:
var ga = new GlideAjax('SCOPE.SCRIPTINCLUDE').setScope('SCOPE');

Using a simple .setScope makes your GlideAjax call work also to scoped script includes 🙂

ITSM Dev
Mega Expert

Thank you Markus Kraus for that tip. Another question was this you wrote above: hint: use $q

How can $q be used with GlideAjax?

 

Markus Kraus
Kilo Sage

You'd have to do some modifications to the link-function:

var $q = formElement.injector().get('$q');
formScope.triggerUIAction = function (action) {
	var g_form = getGForm(formElement);		
	var result = controller.buttonClicked(action, g_form);
	if (result || result === undefined) {
		$q.resolve(result)
			.then(ootbTriggerUIAction.bind(formScope, action));
	}
};

and in the buttonClicked function you can then do an GlideAjax call like this:

c.buttonClicked = function (action, g_form) {
	var deferred = $q.defer();

	var ga = new GlideAjax('MyScriptInclude');
	// ...
	ga.getAnswerXML(function (result) {
		deferred.resolve();
	});

	return deferred.promise;
};

Note: The whole widget is just a very primitive way of implementing support for client side UI Actions in the service portal.
This post does not give a "batteries included" solution to it. This is by design, if you want the batteries, you have to buy them (take a look at Swiss Tool - it has a lot more features than just Client Side UI Actions in the portal).
The goal of this post is to give developers an alternative approach to cloning the OOTB widget.
The reason why i posted this is, that the cloning is a potential security and maintenance risk that most developers are not aware of. When using the technique described here, you still have to some parts of the implementation on your own.

If you are not able to implement it, you are probably also not able to maintain the item (the same applies to the cloning the form widget). In this case you are well advised to use tools that are maintained by an external supplier.

ITSM Dev
Mega Expert

Very cool, thank you for the explanation!

Matt S6
Tera Contributor

Hi @Markus Kraus 

 

Thank you for the write-up! Does this work in Tokyo and Utah? I created my sibling widget exactly as you described and added it to the Form by going through sp_form.list and then sp_instance.list --> create new instance. I see the Client Side Buttons widget is now on my Form page, just below the Form widget.

 

Unfortunately, when I open the form in the Portal, none of my client-side buttons appear. I added an "alert" call to both the Client Controller and Link function. I see both the Client Controller and Link functions are running, and no errors are thrown in the browser JavaScript console.

 

Unfortunately, only my non-client buttons are appearing, which was the case before implementing this widget. Is there anything I may have missed? I also tried implementing in both the Global scope and a custom app scope, and the behavior is the same. Our ServiceNow instances are currently Tokyo; my PDI is Utah so I tested there as well without success.

 

Your example above only shows Client Controller and Link functions; do I need to add anything to the HTML Template or CSS? My thought was that since this is a sibling widget it "piggybacks" off the default Form widget.

 

Matt

Matt S6
Tera Contributor

Hi @Markus Kraus 

 

Thank you for the write-up! Does this work in Tokyo and Utah? I created my sibling widget exactly as you described and added it to the Form by going through sp_form.list and then sp_instance.list --> create new instance. I see the Client Side Buttons widget is now on my Form page, just below the Form widget.

 

Unfortunately, when I open the form in the Portal, none of my client-side buttons appear. I added an "alert" call to both the Client Controller and Link function. I see both the Client Controller and Link functions are running, and no errors are thrown in the browser JavaScript console.

 

Unfortunately, only my non-client buttons are appearing, which was the case before implementing this widget. Is there anything I may have missed? I also tried implementing in both the Global scope and a custom app scope, and the behavior is the same. Our ServiceNow instances are currently Tokyo; my PDI is Utah so I tested there as well without success.

 

Your example above only shows Client Controller and Link functions; do I need to add anything to the HTML Template or CSS? My thought was that since this is a sibling widget it "piggybacks" off the default Form widget.

 

If I create non-client versions of my UI Actions, with just server-side code in them, then your widget works perfectly! My UI Actions run in native with a mix of client and server-side code, hence my desire to have the same functionality in Service Portal.

 

Matt

Markus Kraus
Kilo Sage

Hi @Matt S6 Please undo all changes, and fork the following repository:

https://github.com/kr4uzi/ServiceNow-Portal-Experience

 

EDIT: In regards to your original question: This technology runs purely on the OOTB Form Widget - and the OOTB form widget simply doesn't load any UI Actions with "client = true". There is also no way to make certain Classic UI technology like GlideModal work on the Service Portal. This means you *must* go with client = false (and "portal = true" if you're using the repository above).

 

You will only need to fork the repository and import it "from source control" using the ServiceNow studio.

No need to do any modifications to a page, create widgets and so on. The following will be available:

1.) Client Side UI Actions (there will be a new UI Section called "Portal" and you can choose one of many templates (just enable the form tables by clicking the three dots on the form header)

2.) Form Annotations: Annotations will also load

3.) Data Table Field Styles (Note: Also works with a new Section "Portal" on the Field Style Form)

4.) Data Table UI Actions (same as form, but for Data Table Widgets)

 

Note: No customization have to be done on any OOTB artefacts. This repository has been tested with Utah and is proven to work. You can also download the Portal Experience Verification repository on my github if you want automated verification of the features above.

 

More details on:

https://github.com/kr4uzi/ServiceNow-Portal-Experience/wiki

Matt S6
Tera Contributor

Hi @Markus Kraus 

 

Thank you for the feedback. Indeed I was able to get this working by using "client = false". I had to duplicate my buttons (UI Actions) because I have in the native UI the actions run both client and server code. The new UI Action buttons, which are set to "client = false", run server-side code only. The client-side code I've put into the client controller in the c.buttonClicked function.

 

Basically what I'm doing is providing the user confirmation based on their button clicks, asking them to confirm what they're doing. I've tweaked the conditions of the UI Actions to examine the URL to determine whether the user is interacting with the Portal UI vs. the native UI, so only the appropriate buttons are displayed.

 

Kindest regards,

Matt

Version history
Last update:
‎06-26-2023 02:12 PM
Updated by:
Contributors