Introduction to UI Builder
For those of you not yet familiar with the mysterious and sometimes difficult to cope with, yet very rewarding technology called UI Builder, it is simply a web user interface builder. It allows users to build pages using the Next Experience Components. These pages can then be used across many ServiceNow products, i.e. CSM Configurable Workspace, Custom Web Experience or App Engine Studio generated workspace.
You can read more about it on the official documentation page.
In this article I’d like to guide you through creating your first page in UI Builder, add components, bind data to them and trigger/catch events.
Technology
UI Builder is a very friendly user interface – for those who don’t approach it for the first time as it can also be confusing. I don’t know if it depends on the browser used, but there are some design decisions which I do not fully understand.
As it is a web user interface builder, it uses the WYSIWYG (what-you-see-is-what-you-get) technology. You can place pre-built (or custom, I described the custom component creation in my previous article about GraphQL. Oh, and there will be some GraphQL here too) web components on the page. They can obviously communicate with each other thanks to events. They can also be styled independently. The official documentation page is very detailed and accurate in this case, so if you want to learn more about them, just go there.
Scenario – user info page
OK, let’s start. Imagine a requirement where you have to build a single page for a user, where he can see some basic information about roles and groups assigned. Your developer is not very familiar with service portal and widgets (not to mention Jelly) and the customer doesn’t like the backend view with related lists and forms. The customer has heard about this great tool called UI Builder where non-developers (or citizen developers) can easily prototype what they want to see (which is also a valid use case, thanks to WYSIWYG editor).
Step 1 – It’s all about data
First things first – you have to define your data. You have multiple options and I will write about the most recent one – GraphQL. I will not dive into too much detail here, but if you want to learn more about it, check out my previous article.
Defining data sources
Based on our customer requirements, we can define at least 3 data sources – we need user data, user roles and user groups. With GraphQL, we can simplify it to 2 queries (which, by the way, could have also been written by customer’s business users, because GraphQL is so business friendly.
Query #1 – get user’s data
Here we will combine basic user data with roles and groups:
schema {
query: Query
}
type Query {
getUser(id: ID!): User
}
type User {
id: ID @source(value: "sys_id")
name: String @source(value: "userName")
roles: [Role]
groups: [Group]
}
type Role {
name: String
}
type Group {
name: String
manager: String
}
It’s up to us how we define the scripted resolvers, but the query is pretty straightforward: we ask for a specific user (getUser(id)
) and receive this user’s id, name, roles and groups.
Query #2 – get groups
We need another query to get groups available for the user:
schema {
query: Query
mutation: Mutation
}
type Query {
getGroups: [Group]
}
type Mutation {
addUserToGroup(userID: ID!, groupID: ID!): GroupMember
}
type Group {
id: ID
name: String
manager: String
}
type GroupMember {
userName: String
groupName: String
}
This query contains something new – a mutation. In GraphQL’s world, it simply means that we will modify data, not only retrieve it. The beauty of GraphQL really stands out if you think of how to implement the resolvers here. You probably have multiple ideas how to get groups for the users – this is fine, ServiceNow has always allowed multiple solutions. This API allows you to not only implement multiple logics but also to easily switch between them (by activating/deactivating the resolvers).
The query itself remains clean, readable and not cluttered with script or comments. With GraphQL, everything has its own place – query, resolvers, mapping. At first glance it seems complex, but in fact, when you start working with it, everything becomes clear and you actually are more efficient with no need to scroll through the code.
Mutation #1 – add user to group
The mutation itself is already defined in query #2 above. But because I haven’t mentioned it in my previous article about GraphQL, I’d like to use this opportunity and explain it in more detail. The basics are the same as with the query – the syntax, the mapping between the path and the resolver and the scripted resolver itself. The only thing that is different is within the resolver code – you insert/update data:
(function(process){
const userId = env.getArguments().userID;
const groupId = env.getArguments().groupID;
let newRec = new GlideRecord('sys_user_grmember');
newRec.initialize();
newRec.setValue('user', userId);
newRec.setValue('group', groupId);
newRec.insert();
return {
userName: userId,
groupName: groupId
}
})(env)
GraphQL Data Brokers
Once we have the queries, it’s time for data brokers. It is something that ties together our UI Builder experience and data query. We need one data broker per one query we want to use on our page, no matter if it’s a regular query or mutation.
There are couple of important fields on the GraphQL data broker record:
- Name – unique name used to identify the data broker
- Properties – variables used in query and their metadata. An array of objects, even if it’s only one variable. Can be empty. Example:
[
{
"name": "id",
"label": "User Sys Id",
"description": "User's sys_id",
"readOnly": false,
"fieldType": "string",
"mandatory": true,
"defaultValue": ""
}
]
- Query – GraphQL query. Example:
query ($id: ID!) {
appScope {
schemaName {
getUser (id: $id) {
name
id
roles {
name
}
groups {
name
manager
}
}
}
}
}
- Mutates server data checkbox. Must be checked for mutation queries. You’ll soon see why
Step 2 – it’s all on one page (or is it?)
OK, now we are ready to create our page. The design itself is not that important. There are a few things that I want to describe in more detail.
As I’ve already mentioned, UI Builder is a WYSIWYG editor, so components will look exactly like you see them. There is no proxy. The page white background is the boundary in which you have to fit. On the other hand, you also have to make it look user friendly. Remember about margins, sizes, colors, etc.
Important concepts to understand are page properties and page variants.
Page properties
Every page can have required or optional properties. These are the values that reside in the URL and can be accessed by components or scripts on the page. The difference between them is that required parameters are mandatory and are not preceded by their name, it’s just the value in the URL. An example of required parameter:
Page variants
In simple words, the page variant is exactly what you would imagine – a different version of the page. The final page the end user is viewing does not contain the information about the variant, unless directly added on the page design.
Variants can be defined in two ways – based on conditions or audience record.
- Conditions may contain required parameters constraints
- Audience record creates a role (or multiple roles)
Be aware when using an audience record – if a user fits into multiple audiences, then the platform will not necessarily display the one he should see.
Data connection
Since we have defined the data brokers, we can add them to the page experience. In the data resources tab, select your app and there should be your data brokers, split into regular queries and mutation queries:
Once added, based on properties of the data broker, we will be able to provide mapping for the variables (if we can, not mandatory, especially for the mutating queries):
In this example I am mapping the value of @context.props.userSysId
(which holds the value of userSysId required parameter) to the $id
variable on my GraphQL data query. Almost immediately we can see the result of the query on the next tab. We know exactly if it worked and what was returned. Really helpful as we don’t have to look through the code when we want to use the value of the query.
So we have the page, the components and the data source all in one page. How do we connect it altogether? With the @data
variable.
@data
variable
It is an object with results of our data source queries. In the case of GraphQL queries it makes even more sense, because it’s all nice JSON. The structure is simple:
{
data: {
[data source name]: {
output: {
[result of the query]
}
}
}
}
So in our case, we can put the user name to the component text value with the following line: @data.get_user_data.output.data.[app scope].[schema namespace].getUser.name
Step 3 – components like events
And now the final step – events. Lots of interesting details can be found on the official documentation page.
Events are a way of communication between web components. Events are triggered by some actions (every component has its own set of available actions) and can trigger other actions (display modal, update client state parameter, run client script, …). Single action can trigger multiple events. Events can also be triggered by data source operations – initialization, failure and success. And finally, events can be triggered conditionally.
For the purpose of this custom scenario, imagine a list of groups to which a user can be added and is rendered as a checkbox in a repeater component (so a single checkbox repeated for every value of an input array). Every checkbox item is created with a script:
function evaluateProperty({api, helpers}){
let arr = [];
let ord = 1;
api.data.get_groups.output.data.[app scope].[schema_namespace].getGroups.forEach((g) => {
arr.push({
order: ord++,
checked: false,
itemId: g.id,
label: g.name + ' managed by ' + g.manager,
hasLink: false
});
});
return arr;
}
When a user checks one of the groups, he gets added to it immediately. An event configured for the checkbox component would look like that:
Where @payload is a special variable referring to the currently clicked checkbox item.
In our sample case, the chain of events could be as follows:
- Get user data and Get groups are run immediately after the page loads
- Add user to group is ran on demand, after e.g. button click
- Get user data is refreshed upon successful Add user to group call
Final thoughts
I know, it doesn’t sound simple. There’s a lot of information even in this short article. If it sounds completely new to you, try it yourself. Once you’ve built a couple of pages, added some data and events, then you will understand how it all works and is tied together. And believe me, you will enjoy it. It’s very rewarding, to build on such a complex environment and to provide additional, sometimes really surprisingly powerful user experience.