- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
50m ago
Intro
ServiceNow does not provide an out-of-the-box slider variable type for Catalog Items. However, the platform gives us a flexible way to build our own by using a Custom or Custom with Label variable and rendering it with either a UI Macro or a Service Portal widget.
In this article, we want to show how to build a fully custom numeric slider widget for the Service Portal and use it inside a Catalog Item. My example is the Server Tuning Catalog Item, where a requester can define the disk size in GB using a slider.
The key idea is simple:
- use a Custom / Custom with Label variable
- assign a Service Portal widget
- make the widget reusable by driving it through configurable widget options
- store the selected value in a dependent catalog variable
This approach works very well if you want to introduce custom HTML controls into the Service Catalog experience.
Use Case
In my example, the user should define the size of a disk like this:
- minimum: 10 GB
- maximum: 500 GB
- step size: 5 GB
- initial value: 20 GB
The slider displays the selected value and writes it into another catalog variable, for example:
size_disk_1
That dependent field is important, because in this implementation the custom widget itself is used for rendering and interaction, while the actual value is persisted through a standard variable.
Solution Overview
The solution consists of a few building blocks:
A custom field on Variable [item_option_new]
- u_widget_options
- type: Name-Value Pairs
A reusable Service Portal widget
- name: Service Portal Numeric Slide
A Script Include
- DBOR_WidgetVariableUtils
- reads the widget’s option schema and prepares it for the Variable form
Two client scripts on the Variable table
- one to populate the widget options automatically
- one to copy the option values into the Variable’s default_value
A dependent standard variable
- e.g. size_disk_1
- typically a Single Line Text
- ideally Read only or even hidden
1) Add a configuration field to the Variable table
To make the widget reusable, we added a field to the Variable [item_option_new] table:
- Column name: u_widget_options
- Type: Name-Value Pairs
This field stores the configuration for the widget on the variable itself.
That means you do not have to clone the widget for every slider we want to use. Instead, we can configure each variable individually with values like:
- start value
- minimum value
- maximum value
- step size
- unit
- dependent variable name
Dictionary Entry for u_widget_options
3) Client Scripts on the Variable form
To make the Variable form user-friendly, we added two client scripts.
Client Script 1: populate u_widget_options from the selected widget
Name: DBOR Set Widget Options
This script runs when the widget field changes. It calls the Script Include, reads the widget schema, and builds a JSON object with the default values.
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue === '') {
if (newValue == '') {
g_form.setValue('u_widget_options', "");
}
return;
}
if (!g_form.getElement('u_widget_options')) {
g_form.addErrorMessage(getMessage('dbor_sc_missing_widget_options_on_form'));
}
var ga = new GlideAjax('DBOR_WidgetVariableUtils');
ga.addParam('sysparm_name', 'getOptions');
ga.addParam('widgetSysId', newValue);
ga.getXMLAnswer(getResponse);
function getResponse(response) {
var options = JSON.parse(response);
var values = {};
for (var o in options) {
var option = options[o];
if (option.key) {
var v = option.default_value;
values[option.key] = (v) ? v : "";
}
}
g_form.setValue('u_widget_options', JSON.stringify(values));
}
}What it does
- clears the field if no widget is selected
- warns the admin if u_widget_options is not on the form
- fetches the option schema via GlideAjax
- creates a default JSON payload
- stores it in u_widget_options
Client Script 2: mirror widget options into default_value
Name: DBOR Set Widget Options in Default Value
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading) {
return;
}
g_form.setValue('default_value', newValue);
}Why this helps
This small script copies the widget option JSON into the variable’s default_value. In this setup, that is a practical way to keep the configuration stored directly with the variable.
4) Script Include: reading the widget option schema
To support the administration experience on the Variable form, I created a Script Include called:
DBOR_WidgetVariableUtils
Its purpose is to read the widget’s option_schema from the sp_widget record and return it as JSON
var DBOR_WidgetVariableUtils = Class.create();
DBOR_WidgetVariableUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
/**
* Return Options of Widget as JSON string. String includes array of options. Option format:
* {
* 'key': option.name,
* 'label': option.label,
* 'hint': option.hint,
* 'default': option.default_value
* }
*/
getOptions: function() {
var result = [];
var sys_id_widget = this.getParameter('widgetSysId');
var gr = new GlideRecord('sp_widget');
if (gr.get(sys_id_widget)) {
var options_schema = gr.getValue('option_schema');
var options = JSON.parse(options_schema);
for (var i in options) {
var option = options[i];
var value = option.default_value;
var retOption = {
'key': option.name,
'label': option.label,
'hint': option.hint,
'default_value': (value) ? value : "",
'type': option.type
};
result.push(retOption);
}
}
return JSON.stringify(result);
},
type: 'DBOR_WidgetVariableUtils'
});Why this Script Include matters
This is the heart of the configuration model.
Instead of hardcoding all available widget options somewhere else, the Script Include reads them directly from the widget definition. That keeps the solution clean and reduces duplication.
If I later add another option to the widget schema, I only need to update the widget once.
5) Define the widget option schema
The widget record contains an Option Schema that describes which options are available.
For this slider, the schema contains:
- start_value
- minimum_value
- maximum_value
- step_value
- unit
- with_value_label
- dependent_variable_name
Formatted, it looks like this:
[
{
"hint": "Value to start with",
"name": "start_value",
"section": "Data",
"default_value": "0",
"label": "Start Value",
"type": "integer"
},
{
"hint": "Minimum value of slider",
"name": "minimum_value",
"section": "Data",
"default_value": "0",
"label": "Minimum Value",
"type": "integer"
},
{
"hint": "Maximum value of slider",
"name": "maximum_value",
"section": "Data",
"default_value": "10",
"label": "Maximum Value",
"type": "integer"
},
{
"hint": "Steps of slider",
"name": "step_value",
"section": "Data",
"default_value": "1",
"label": "Step Value",
"type": "integer"
},
{
"hint": "Unit which should be shown behind the value",
"name": "unit",
"section": "other",
"default_value": "",
"label": "Unit",
"type": "string"
},
{
"hint": "Defines if the selected value should be shown on right bottom of slider",
"name": "with_value_label",
"section": "Data",
"default_value": "true",
"label": "With Value Label",
"type": "boolean"
},
{
"hint": "Name of variable which should be populated by the widget.",
"name": "dependent_variable_name",
"section": "Behavior",
"label": "Dependent Variable Name",
"type": "string"
}
]This is the foundation that makes the widget configurable and reusable.
A real example for the Variable default_value / widget option JSON looks like this:
{
"start_value": "20",
"minimum_value": "10",
"maximum_value": "500",
"step_value": "5",
"unit": "GB",
"with_value_label": "true",
"dependent_variable_name": "size_disk_1"
}
6) The widget: Service Portal Numeric Slide
Now to the actual widget.
The widget is intentionally simple:
- the Server Script prepares the data
- the Client Script / Controller reacts to slider movement
- the HTML template renders a standard <input type="range">
- the CSS provides basic styling
Service Portal Numeric Slide Widgett
Server Script
The Server Script reads the widget options and prepares the values for the template and controller.
(function() {
/* populate the 'data' object */
/* e.g., data.table = $sp.getValue('table'); */
data.min = options.minimum_value;
data.max = options.maximum_value;
data.step = options.step_value;
data.unit = options.unit;
var middle = (data.min + data.max) / 2;
data.default_val = (options.start_value) ? options.start_value : middle;
data.with_value_label = options.with_value_label == "true" ? true : false;
data.dependent_variable = options.dependent_variable_name;
var id = (data.dependent_variable) ? data.dependent_variable : "";
data.slidername = id + "_wgt_slider";
data.sliderId = id + '_wgt_slider';
data.valueId = id + '_wgt_value';
})();What the Server Script does
This is the part that “prepares” the widget.
It reads:
- minimum value
- maximum value
- step size
- unit
- start value
- dependent variable name
- whether the value label should be shown
It also creates unique IDs:
- slidername
- sliderId
- valueId
This is especially important if you place multiple sliders on the same Catalog Item. Without unique names and IDs, one slider could interfere with another.
A particularly useful detail is this fallback:
var middle = (data.min + data.max) / 2;
data.default_val = (options.start_value) ? options.start_value : middle;If no start value is defined, the widget uses the midpoint between min and max.
Client Script / Controller
The widget logic lives in the client controller.
api.controller = function($rootScope, $scope) {
/* widget controller */
var c = this;
$rootScope.$on('spModel.gForm.rendered', function() { // page fully loaded
var slider = document.getElementById(c.data.sliderId);
var output = document.getElementById(c.data.valueId);
var dep_var = c.data.dependent_variable;
if (dep_var && dep_var != "") {
$scope.page.g_form.setValue(dep_var, c.data.default_val);
}
slider.oninput = function() {
output.innerHTML = this.value;
if (dep_var && dep_var != "") {
$scope.page.g_form.setValue(dep_var, this.value);
}
};
});
};
What the controller does
This is the interactive part of the widget.
Once the catalog form is fully rendered:
- it finds the slider element
- it finds the label element showing the current value
- it reads the configured dependent variable
- it initializes that dependent variable with the start value
- on every slider movement, it:
- updates the displayed value
- writes the current value into the dependent variable
This design makes the slider behave like a real form control, while still using a standard backend field to persist the selected value.
Why the dependent variable is needed
In this implementation, the selected slider value is written into another catalog variable, for example:
size_disk_1
That is the field you can later read in workflows, flows, request processing, or fulfillment logic.
This is also the main trade-off of this approach: the custom widget is great for rendering and interaction, but you still need a standard field to reliably persist and consume the value downstream.
HTML Template
The template is intentionally minimal and uses a native HTML range input.
<div class="numeric_slider">
<input type="range"
id="{{c.data.sliderId}}"
class="slider"
name="{{c.data.slidername}}"
value="{{c.data.default_val}}"
step="{{c.data.step}}"
min="{{c.data.min}}"
max="{{c.data.max}}" />
<div class="slider_text" ng-show="c.data.with_value_label">
<span id="{{c.data.valueId}}">{{c.data.default_val}}</span> {{c.data.unit}}
</div>
</div>This is a good reminder that a custom catalog variable can render any HTML control you want.
CSS tips
Styling is not the most important part of this article, but a little CSS helps make the control look cleaner.
.numeric_slider {
width: 100%;
}
.slider {
width: 100%;
height: 25px;
outline: none;
opacity: 0.7;
border-width: 1px;
border-radius: 15px;
transition: opacity .2s;
}
.slider:hover {
opacity: 1;
}
.slider_text {
width: 100%;
margin-top: 0.2em;
padding-right: 0.5em;
text-align: right;
}Small styling recommendations
A few practical tips:
- keep the slider at width: 100%
- right-align the value label if you want a clean catalog layout
- use subtle hover effects only
- avoid over-styling unless there is a real UX need
For a technical article like this one, basic styling is enough.
7) Variable configuration in the Catalog Item
On the Catalog Item, I configured the slider variable like this:
- Type: Custom with Label
- Widget: Service Portal Numeric Slide
Variable using Slider Widget
The option values are then stored/configured in the widget options as key-value pairs, for example:
Widget selection and Slider Options
This makes the widget universal and reusable.
😎The dependent field
To persist the value, we create a normal variable:
- Type: Single Line Text
- Name: size_disk_1
I recommend making this variable Read only.
If the field is visible and editable, users could change it manually, and then the slider and the field might get out of sync.
At the moment, the implementation only updates the field from the slider.
Dependend Variable which is updated by the slide
If you want two-way synchronization, you would need an additional script that updates the slider when the field value changes.
9) Important limitation
This solution works in the Service Portal only.
Because the rendering relies on a Service Portal widget, the same variable will not automatically work in the classic backend catalog UI.
10) Final result
The result is a clean, reusable slider variable for the Service Catalog:
- easy for users to interact with
- configurable per variable
- reusable across multiple Catalog Items
- based on standard web technology
- flexible enough to support many other custom controls
The GIF below shows the final behavior: the slider moves, the visible value updates, and the dependent field is populated at the same time.
Slider in action
11) Conclusion
If you need a catalog control that does not exist out of the box, using a Custom variable with a Service Portal widget is a very effective pattern.
- 83 Views