- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
Preamble:
Previously the co-author of this solution @Martin Virag wrote an article on how you can speed up diagramming with the help of AI . However, the approach detailed there have had one flaw. It did not run on ServiceNow!
Therefore we decided to build a custom application that allows you to generate diagrams by using Draw.io and an AI agent running on the now platform.
The solution consist a custom UI Builder page where we have introduced a custom Agent Component and a Draw.io integration.
In the next part of our article series, we would like to present one of the main components of our solution: the Draw.io integration. This feature allows users to create diagrams and save them in the ServiceNow database without leaving the platform.
Draw.io (diagrams.net)
Let’s start from a broader perspective. Draw.io does not really need an introduction: it is a free diagram creation tool, that can be used both in a web browser and as a desktop application. Most of the diagram types are supported.
The developers of the product also realized, that productivity increases when the tool can work together with other systems. This is achieved by embedding the application using an iFrame component. (Just let's think about the Confluence - Draw.io integration.) The communication between the parent window and the iFrame HTTP element is based on a technology, that may seem surprisingly old. Browsers have supported interaction between these two elements since the 2010s, using the JavaScript postMessage() function to send data and by listening to the "message" event to receive incoming information.
We took this feature of Draw.io as a basis when developing our solution.
Communication between Parent page and iFrame component
In the following section, we will go into the technical details through a ServiceNow-based example to build the knowledge, which is needed to understand the functions used in our solution. It is important to present this case using ServiceNow, because at the same time it also shows what kind of support the platform provides for this type of implementation.
A simple UI Page will be used in the example which is embedded into the iFrame on both Service Portal and Workspace layouts.
Build the UI Page
Let's start building a very simple the UI Page, using the most generic "Hello World" content:
Once the UI Page is done, need to embed it into an iFrame component, which is part of a portal widget.
The result so far looks like the following:
We need to do the same action in the UI Builder as well. First a page and a variant has to be created than the iFrame component with the reference page.
The result of our work can be seen below:
The message content type is JSON. Only two attributes are used, one for the message content (msg) and the second one is the actual date (date).
As I mentioned earlier, postMessage() is used to send messages, and the message event is used to receive them.
Remember, the UI page is in an iFrame, so we have to use the postMessage function from the parent component. Otherwise, the parent cannot receive the message.
Another important element is the origin. This is a security setting used to check if the data comes from a valid source. If the check fails, the message will not be processed.
Let's take a look at the source code.
HTML code of the UI Page:
<?xml version="1.0" encoding="utf-8" ?>
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide" xmlns:j2="null" xmlns:g2="null">
<style>
.container {
max-width: 300px;
display: flex;
flex-direction: column;
gap: 10px;
}
input, textarea {
width: 100%;
box-sizing: border-box;
}
button {
width: 150px;
}
textarea {
resize: vertical;
height: 300px;
}
</style>
<h3>Child element in iFrame</h3>
<div class="container">
<input id="chtInput" type="text" class="form-control" />
<button onclick="sendMessageToParent(document.querySelector('#chtInput').value);" class="btn btn-default">Send to parent</button>
<textarea id="chtArea" rows="7" class="form-control"></textarea>
</div>
</j:jelly>
JavaScript code of the UI Page:
// This is the listener of incoming messages
window.addEventListener("message", (event) => {
// The incoming data is a JSON, this is a built-in SN feature
if (event.origin === window.location.origin) {
let message = "Parent:\t(" + event.data.date + ") " + event.data.msg + "\n";
document.getElementById("chtArea").value += message;
} else {
console.warn("The incoming message is ignored, because of invalid origin.");
}
});
/**
* This function is responsible for sending the message from iFrame to parent
*/
function sendMessageToParent(chtMessage) {
let message = {
msg: chtMessage,
date: getCurrentDateTime()
};
// Set the origin to be on the safe side.
let origin = window.parent.location.origin;
window.parent.postMessage(message, origin);
let pMessage = "Me:\t(" + message.date + ") " + message.msg + "\n";
document.getElementById("chtArea").value += pMessage;
document.getElementById("chtInput").value = "";
}
/**
* Simple function in order to get the date in a correct format
*/
function getCurrentDateTime() {
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const HH = String(now.getHours()).padStart(2, '0');
const MM = String(now.getMinutes()).padStart(2, '0');
const SS = String(now.getSeconds()).padStart(2, '0');
return yyyy + "." + mm + "." + dd + ". " + HH + ":" + MM + ":" + SS;
}
SP widget source
HTML code:
<div>
<div class="parent-elem">
<div class="iframe-elem">
<iframe id="testIFrame" src="/iframe_child_page" class="iframe-test" />
</div>
<div>
</div>
<div class="container">
<h4>
Parent element
</h4>
<input id="chtInput" type="text" class="form-control" ng-model="c.chatMsg" />
<button ng-click="c.sendMessageToChild();" class="btn btn-default">Send to child</button>
<textarea id="chtArea" rows="7" class="form-control" ng-model="c.chatArea"></textarea>
</div>
</div>
</div>
Client controller:
api.controller=function($scope) {
/* widget controller */
var c = this;
c.chatMsg = "";
c.chatArea = "";
// This is the message listener
window.addEventListener('message', function (event) {
$scope.$apply(function () {
// This is important for security reason
if (event.origin === window.location.origin) {
let pMessage = "Child:\t(" + event.data.date + ") " + event.data.msg + "\n";
c.chatArea += pMessage;
}
else {
console.warn("The incoming message is ignored, because of invalid origin.");
}
});
});
// Sending message to iFrame
c.sendMessageToChild = function() {
console.log(c.chatMsg)
const iframe = document.getElementById('testIFrame');
let message = {
msg: c.chatMsg,
date: getCurrentDateTime()
};
iframe.contentWindow.postMessage(
message,
window.location.origin
);
c.chatMsg = "";
let pMessage = "Me:\t(" + message.date + ") " + message.msg + "\n";
c.chatArea += pMessage;
};
/**
* Get date information in a correct format
*/
function getCurrentDateTime() {
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const HH = String(now.getHours()).padStart(2, '0');
const MM = String(now.getMinutes()).padStart(2, '0');
const SS = String(now.getSeconds()).padStart(2, '0');
return yyyy +"."+ mm +"."+ dd +". "+ HH +":"+ MM +":"+ SS;
}
};
UI Builder page
The structure of the UI Builder page is also simple, but the configuration is slightly more complex than the SP widget, so let’s go through it step by step.
The following state parameters are required:
-
chatText: this variable stores the current text
-
iFrameContent: this value is sent to the iFrame component
-
textAreaContent: this contains the chat message shown on the screen
-
pageOrigin: this setting is important for secure message communication (I have already explained this attribute.)
Three client scripts are responsible for the chat feature:
1. Process incoming message
function handler({api, event, helpers, imports}) {
console.log(event);
if (event.payload.data.msg) {
let pMessage = "Child:\t(" + event.payload.data.date + ") " + event.payload.data.msg + "\n";
api.setState("textAreaContent", api.state.textAreaContent + pMessage);
}
}
2. Send message to iFrame element
This script is responsible for sending data and is triggered when the button is clicked. The UI Builder includes a built-in messaging feature for the iFrame component, which requires the use of the Data and Target origin parameters.
The message is sent automatically as soon as the value of the Data parameter changes.
⚠️ Important information: while the postMessage() function itself can handle objects of any type, the UI Builder iFrame component only works with JSON. In the case of Draw.io, this limitation causes an issue, which will be discussed later.
function handler({api, event, helpers, imports}) {
let message = {
msg: api.state.chatText,
date: getCurrentDateTime()
};
console.log(message);
api.setState("iFrameContent", JSON.stringify(message));
let pMessage = "Me:\t(" + message.date + ") " + message.msg + "\n";
api.setState("textAreaContent", api.state.textAreaContent + pMessage);
api.setState("chatText", "");
function getCurrentDateTime() {
const now = new Date();
const yyyy = now.getFullYear();
const mm = String(now.getMonth() + 1).padStart(2, '0');
const dd = String(now.getDate()).padStart(2, '0');
const HH = String(now.getHours()).padStart(2, '0');
const MM = String(now.getMinutes()).padStart(2, '0');
const SS = String(now.getSeconds()).padStart(2, '0');
return `${yyyy}.${mm}.${dd}. ${HH}:${MM}:${SS}`;
}
}
3. Set origin
As mentioned earlier, the Target origin parameter of the iFrame component is an important factor from a security perspective. If the origin of the sender and the receiver is different, the message delivery will fail.
function handler({api, event, helpers, imports}) {
api.setState("pageOrigin", helpers.getBaseUrl);
}
The script above runs when the page is loaded. It is enough to get this value once, and it can then be reused for each request.
Let’s take a high-level overview of the configuration.
The following animations show how the chat feature works in both Service Portal and Workspace.
Using iFrame and Parent communication on Service Portal
Using iFrame and Parent communication on Workspace
Diagrams with Draw.io (diagrams.net)
Now let’s move on to the actual solution: the integration of Draw.io.
There is a lot of useful material available on this topic, but I would like to highlight the following two sources: LINK, LINK
As it was shown in the previous section, the UI Builder iFrame component supports communication, which made it possible to embed and use Draw.io.
Let’s take a look at how this interaction works.
When Draw.io is loaded into the iFrame using a properly constructed URL (for example:https://embed.diagrams.net?param1=...¶m2=...), the application sends a message to the parent window (init) as soon as it is ready for use.
The second step is to decide whether to start with a blank canvas or load an existing diagram.
The loading feature is quite limited, as only the Draw.io data format (mxfile) can be used. Unfortunately, the import model (like Mermaid diagram) option is not supported.
Diagrams created in Draw.io can be saved and later reloaded, and there is also an option to export them.
During development, we used all the operations which can be seen above.
In every communication, JSON data is exchanged between the two components. It is important to mention that when the UI Builder sends information to the iFrame component, it always converts the package into a JSON object. However, Draw.io expects a text message and parses it as the first step. This causes a type mismatch that needed to be handled.
We used a clever workaround: we applied the JSON.stringify() function twice to the JSON object. The iFrame’s built-in logic performed the first parsing, and the Draw.io receiver module performed the second one. After this, the embedded component started working correctly.
We wanted to keep the use of the Draw.io API at a low level, so we created a wrapper solution. From the UI Builder point of view, this appears as a single UI component. The component communicates with its parent through different events, which enables a much simpler and cleaner interaction.
The main behavior of our Draw.io component can be configured using component-based properties. These properties can be seen on the image below:
Working solution
From this point onwards, the communication between the parent window and our Draw.io component was already working, as well as the connection between the component and the embedded Draw.io.
The video below demonstrates how it works with a simple data opening process.
Closing thoughts
It is definitely important to emphasize that we learned a lot from this project. On one hand, we explored the possibilities of embedding Draw.io directly into a page. On the other hand, we tried and learned one of the key new features of ServiceNow Zurich release: the ability to create and develop custom components in UI Builder.
We managed to build a working solution that can significantly speed up tasks related to documentation.
Of course, we faced many challenges along the way, and solving them sometimes required a small effort and sometimes a lot of determination.
I hope this article helps to understand how ServiceNow supports communication with pages opened in an iFrame, and it also shows the wide range of use cases the platform offers.
- 39 Views
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
