Making attachment mandatory from client script service portal catalog

lauri457
Giga Sage

Ever had the requirement of conditionally making the attachments mandatory on the portal catalog item page?  There is the attachment variable but I've seen business reject it as an option more often than not and there are multiple posts on the community to query the dom directly for the attachment list e.g:

this.document.getElementsByClassName('get-attachment').length

The attachment variable does not allow multiple attachments and the attachment is not attached to the RITM. The dom query makes for bad ux as it alone will not display the asterisk similar to the Mandatory Attachment checkbox.

lauri457_0-1765930036799.png

I wanted to achieve above oob functionality but control it without making changes to any of the related widgets so I had a look at how the widget works and turns out it can be achieved quite easily.

 

Looking at the SC Catalog Item widget

In the catalog item widget clicking the order button calls a function from the scope called triggerOnSubmit which does the submit with GlideForm, but the function itself does not handle much else. From the asterisk elements we can see that they are controlled by the mandatory_attachment boolean returned from $sp.getCatalogItem() which is controlled by the checkbox in the portal settings

<span class="fa fa-asterisk mandatory" ng-if="data.sc_cat_item.mandatory_attachment" ...>

There is no relevant reference to the mandatory_attachment property in the widget itself so looking at where the data.sc_cat_item object is passed as an argument we can see that it is bound into the item attribute of the spCatItem directive.

<sp-cat-item item="::data.sc_cat_item"></sp-cat-item>

Inside the link function of the spCatItem directive the item is stored in the scope and how the functionality works can be seen in the event handler that is registered to the GlideForm submit event. If there are no attachments an errormessage is displayed and the submission is canceled by returning false from the handler.

g_form.$private.events.on('onSubmit', function () {
// ... removed script
	if ($scope.item.mandatory_attachment) {
		if (!$scope.item.attachment_submitted && !($scope.attachments && $scope.attachments.length > 0)) {
			var _response = spUtil.addErrorMessage(scMandatoryAttachMessage);
			var handledMessage = _response !== false;
			if (!handledMessage)
				spModal.alert(scMandatoryAttachMessage);
			return false;
		}
	}
	return true;
});

 

Solution

This all means that to trigger mandatory attachments we should only need to set 

$scope.data.sc_cat_item.mandatory_attachment to true/false in the catalog item widget scope. I did not want to edit the widget itself so to achieve this from a catalog client script we can use angular.element() which returns a jquery wrapper and angularjs adds a method to the returned object that allows us to get the scope and manipulate it, we just need to allow access to the window object from the script by unticking isolate script. We can use the first element with id sc_cat_item inside the directive to access scope
this.angular.element("#sc_cat_item").scope().data.sc_cat_item.mandatory_attachment
 
To use this in a client script we just need to change the value in the scope and then make sure the view is updated afterwards to show the mandatory asterisk. Angularjs won't automatically trigger a digest cycle if you update a scope value from the outside so we can safely cause a digest to happen by scheduling the function with $scope.$evalAsync. You could also call $apply for the same effect. 
//isolate script should be false
function onChange(control, oldValue, newValue, isLoading) {
	if (isLoading) {
		return;
	}
	const attachMandatoryGa = new GlideAjax("CatalogSI");
	attachMandatoryGa.addParam("sysparm_name", "mandatoryAttachment");
	attachMandatoryGa.addParam("sysparm_value", newValue);
	attachMandatoryGa.getXMLAnswer((answer) => {
		const catitemScope = this.angular.element("#sc_cat_item").scope();
		catitemScope.$evalAsync(() => {
			catitemScope.data.sc_cat_item.mandatory_attachment = answer === "true";
		});
	})
}
 

Conclusion

The solution should be fairly robust as the $scope.data.sc_cat_item does not get updated at all once it is set so changes to the object will persist. This allows us to achieve better ux matching a common business requirement.

7 REPLIES 7

Are you using $scope.$evalAsync and a callback function or $scope.$apply()? The error means there is a digest in progress already. Can't say exact cause without seeing scripts.

 

If it's doing that even when its async you can try with $applyAsync (this should cause to wait for next digest cycle as opposed to next dirty loop of current cycle) or using the $timeout service if they work any better?

	const $injector = this.angular.element(this.document.body).injector();
	const $timeout = $injector.get('$timeout');
	$timeout(() => {
		catitemScope.data.sc_cat_item.mandatory_attachment =
			!catitemScope.data.sc_cat_item.mandatory_attachment;
	});

 

OlaN
Tera Sage
Tera Sage

Hi,

Interesting solution.

But if the use case is only to make an attachment mandatory based on some other conditions on the form, I would rather add an attachment variable, and make it mandatory using Catalog UI policy or Catalog Client Script, just like any other variable.

This is a valid point and I agree but as I mentioned in the start of the post it has been more than once that business rejects the use of the attachment variable as it doesn't automatically relate the attachment to the created record and that it allows only one attachment.

 

You could also expand from this as you already have the scope and even check some condition against the list of attachments

angular.element("#sc_cat_item").scope().attachments
[
	{
		"ext": "txt",
		"size_bytes": "3 B",
		"encryption_context_display": "",
		"file_name": "test.txt",
		"sys_updated_on_display": "2025-12-17 16:09:57",
		"sys_created_by_display": "System Administrator",
		"canWrite": true,
		"sys_updated_on": "2025-12-17 05:09:57",
		"table_name": "sc_cart_item",
		"encryption_context": "",
		"sys_id": "fdd5fbaa83b57610557ff0d6feaad3c6",
		"content_type": "text/plain",
		"size": "3 B",
		"sys_created_on": "2025-12-17 05:09:56",
		"canDelete": true,
		"table_sys_id": "fb3b272a83357610557ff0d6feaad34e",
		"state": "available",
		"sys_created_by": "admin",
		"trackByKey": "fdd5fbaa83b57610557ff0d6feaad3c6availabletest.txtundefined"
	}
]