/ Development  

A tighter customization with the brand-new JavaScript API

Hi there AppWorks fans,

Welcome to a new installment of AppWorks tips.

This week we do a dive on the new JavaScript API delivered with the latest and greatest 21.4 release! Will it be great!? Heal YEAH!…It will be the future for extending the platform with nicely crafted customizations. Why? Well, what we are about to see in this post is a great JavaScript object called a ‘Promise’ which handles our requests a different way then what we normally would do with extensions done via the HTML5SDK or all the nice scripting in xForm documents. Read all about this ‘Promise’ object here, but in short: The “Promise” object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Key here is the asynchronous call which makes it possible to response on it (in a later moment in time) when the result is eventually received. This makes it more flexible to develop in a more reactive way based on what is retrieved in the response. Can we see it as an event-based way of programming? Well, I don’t think so. The big difference with events is that you register them somewhere upfront to wait for a callback which is not the same as an a-synchronized call which ‘smells’ like a quicker/tighter response.

Don’t dive too much in all the comparison on other approached, but let’s focus on what is delivered with the new JavaScript API. Time to build some interesting samples and see how we can use the API for our own benefits on the solutions we build.


Let get right into it…

Before we’ll dive into all the greatness I would like to start with a set of questions upfront:

Where to get more information?

Interesting resource for starting this post was found here. Also this resource helped me out on some open questions from my side! The platform itself is also delivered with documentation on the new JS API in the ‘OpenText AppWorks Platform 21.4 Advanced Development Guide.pdf’ under the section “Using the application JavaScript APIs”

At OpenText I start to hear a rumor that documentation, samples sources, and demo content will be more shared via the platform developer.opentext.com. So, there is a move going on for sharing content more into the public domain which is in my opinion a great step for us developers. 🤗

Will it run outside the runtime UI?

The documentation is pretty clear on that one. Yes, it’s stated we should be able to perform actions from outside the application…Interesting!

How about authorization?

Well, when we run our custom code from within AppWorks runtime (like a web-content panel in a layout), it will probably use the authentication of the current user, but how about authentication outside the runtime? We’ll try that one out to see what will happen, and how to solve it.

Do we need to include any JavaScript library?

I can’t find anything about it in the documentation, but when I see sample code-lines like this window.parent.publicAPIProvider, it looks like we’re in an iFrame (like a web-content panel on a layout). We’ll have a look in the developer console to see what we can find on this publicAPIProvider object.


Our first steps to make a custom page available

For this one I follow my own post about the HTML5SDK. Not the full post, but the part where we introduce the ‘Web Library Definition’ for an ‘assets’ folder which make it possible to publish HTML and JavaScript files into runtime. I also followed the part where we start with a first playground.htm (called from a web-content panel on a layout of a ‘Person’ entity), and the part where we connect our first JavaScript file playground.js.

This is how it looks like in my current project:

api_001

After publication, double-check for these available artifacts in runtime:

playground.htm

The runtime URL will look like this: http://192.168.56.107:8080/home/appworks_tips/html/playground.htm

Initial content looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<link rel="stylesheet" href="../app/start/web/startup.css" type="text/css" />
<script src="/cordys/thirdparty/jquery/jquery.debug.js" type="text/javascript"></script>
<script src="./../js/playground.js" type="text/javascript"></script>
</head>
<body>
<h1>New API demo</h1>
<p>This is a paragraph.</p>
<input id = "btnClickMe" type="submit" value="Click me"/>
</body>
</html>

playground.js

The runtime URL will look like this: http://192.168.56.107:8080/home/appworks_tips/js/playground.js

Initial content looks like this:

1
2
3
4
5
$(document).ready(function() {
$("#btnClickMe").click(function(){
console.log('Hello world...');
});
});

‘Person’ entity

The ‘Person’ entity has these “play-around” properties applied (and all the default generated BB’s to start prototyping):

Name Label Type
prs_name Name Text
prs_age Age Integer
prs_avatar Avatar Image
prs_salary Salary Currency
prs_date_of_birth Date of birth Date
prs_is_active Is active Boolean
prs_id Identity Text

The Default ‘View’ layout for the entity looks like this (with the web-content panel to show our ‘playground.htm’)

api_002

For you the copy the URL: ./../../../html/playground.htm?itemid={item.Identity.ItemId}

In runtime, it will look like this after we’ve created and open a new instance of the ‘Person’ entity:

api_003

You can clearly see our custom playground customization, and when we click that button we will see some console logging passing by…

Great! Playground is open for further extension.


Get a grip on the “itemId”

In the URL of the web-content panel we already passed the ‘itemId’ of the current entity as parameter in the URL. When you inspect the elements in runtime, you’ll see the web-content panel is just an iFrame loaded with a URL like this:
http://192.168.56.102:8080/home/appworks_tips/html/playground.htm?itemid=080027e84f06a1ec8deeaa8dab74a43a.1&homepageIdData=90B11C6A8EBB11E6F0E862F2E28B7B87

Time to extract the ‘itemId’ from the URL in our own JavaScript with an update like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$(document).ready(function() {
$.urlParam = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results==null) {
return null;
} else {
return results[1] || 0;
}
};

$("#btnClickMe").click(function(){
console.log('Hello world...');
console.log($.urlParam('itemid'));
});
});

After publication and a refresh in runtime, we should see something like this:

api_004

We’re getting closely for a first call on the publicAPIProvider object!

Let’s do another update in the JavaScript:

1
2
3
4
$("#btnClickMe").click(function(){
console.log('Hello world...');
window.parent.publicAPIProvider.updateData($.urlParam('itemid'), 'prs_id', 'hello id!!');
});

Publish, and retest with a tightly interactive result:

api_005

NICEEEEEE! 😍

Next step…


Make the ‘Identity’ a random ID

Just a JavaScript update like this will make it happen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$(document).ready(function() {
$.urlParam = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results==null) {
return null;
} else {
return results[1] || 0;
}
};

$.makeid = function(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for ( var i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};

$("#btnClickMe").click(function(){
console.log('Hello world...');
window.parent.publicAPIProvider.updateData($.urlParam('itemid'), 'prs_id', $.makeid(5));
});
});

After publication, it’s time to hit the ‘Click me’ button till you’re satisfied with a suitable Identity ID! 🤠


Explore the object “publicAPIProvider”

Let’s have a look in the developer console of my Chrome browser (<F12>) while you have the runtime open. Jump to the ‘Console’ tab and have a look in at the API via this command: window.parent.publicAPIProvider:

api_006

We can clearly see the available functions which we can use in our custom page…Let’s have a play with the other calls in the next sections!

Just a note for your information:

You also see the object is available from the JavaScript library coreBundleAwpClient-9e875f5dfe.js behind this URL
http://192.168.56.107:8080/home/appworks_tips/app/start/web/bundles/coreBundleAwpClient-9e875f5dfe.js and is served from this location on the server: /opt/tomee/apache-tomee-plus-8.0.6/webapps/home#app#start/web/bundles/coreBundleAwpClient-9e875f5dfe.js

It looks like minified/obfuscated JavaScript, but still interesting to have a view on it especially when you pretty-print it again:

api_007


Function call ‘saveAllItems’

This is an easy extension on top of our current ‘Click me’ button. Let’s extend the script with just one line:

1
2
3
4
5
$("#btnClickMe").click(function(){
console.log('Hello world...');
window.parent.publicAPIProvider.updateData($.urlParam('itemid'), 'prs_id', $.makeid(5));
window.parent.publicAPIProvider.saveAllItems();
});

What do we see in runtime after a publication and hitting our button again? Exactly, a direct (auto-)save action on the form!

api_008

Simple and effective…Next one!

HINT: This save action also triggers the ‘event’ type of Rule BBs defined for the entity!


Function call ‘getItemData’

This one ‘smells’ like a metadata read-action on my ‘itemId’ property! This way I don’t need to pass all the needed values through the parameters of the URL of the web-panel (into the iFrame). I only require the ‘itemId’ and have a call on getItemData() to retrieve the rest. Let’s try that one out with a simple form div-element like this in our playground.htm file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
...
<div class="form-container" style="padding-top: 10px">
<div id="formName" style="padding-bottom: 8px">
<input class="ot-control" id="inputName" placeholder="Name" maxlength="64"/>
</div>
<div id="formAge" style="padding-bottom: 8px">
<input class="ot-control" id="inputAge" placeholder="Age"/>
</div>
<div id="formIdentity" style="padding-bottom: 8px">
<input class="ot-control" id="inputIdentity" placeholder="Identity" maxlength="64"/>
</div>
</div>
</body>

Time to extend the JavaScript content for our playground.js file at the end of the ‘ready’ function:

1
2
3
4
5
6
$(document).ready(function() {
...
window.parent.publicAPIProvider.getItemData($.urlParam('itemid')).then((itemData) => {
console.log(itemData);
});
}

After publication, we see our valuable metadata passing by in the console log:

api_009

Well, now it’s easy to update my form-fields with an update like this:

1
2
3
4
5
6
window.parent.publicAPIProvider.getItemData($.urlParam('itemid')).then((itemData) => {
console.log(itemData);
$('#inputName').attr('value', itemData.Properties.prs_name);
$('#inputAge').attr('value', itemData.Properties.prs_age);
$('#inputIdentity').attr('value', itemData.Properties.prs_id);
});

After publication and a retest, you will also have a green checkmark! ✅

As the platform uses the ‘Aurelia’ framework for the runtime UX it should also be possible to make it all a bit clever with field-bindings, but my OOTB JavaScript knowledge limits with the basic functionalities of jQuery. I’m always eager to learn new stuff, but this was just one step too far for now! Anyone with an update on this JavaScript implementation? I’m available to have a nerdy chat about it. 🤓


Function call ‘performAction’

Interesting, but what actions are available? Sounds to me like the Rule BB of type ‘Action’ on our entity! Let’s create a simple one like this (and publish it):

api_010

We just add a second button into our current playground.htm file…

1
<input id="btnDoAction" type="submit" value="Do action"/>

…and add a second function into the ‘ready document’ method:

1
2
3
4
5
6
$("#btnDoAction").click(function() {
let result = window.parent.publicAPIProvider.performAction($.urlParam('itemid'), 'a_set_prop_is_active');
if(result) {
console.log('Action done!');
}
});

After publication, we’ll have a nice result!

api_011

HINT: With the execution of the rule it’s also possible to have an interaction with the ‘normal’ entity modeling. Have a look again on the created ‘Rule’ BB where we have the option to show a modal popup before the rule action is performed!


Function call ‘navigate’

Time to navigate around in the runtime UI. We can navigate directly to a homepage ID, and we are able to navigate directly to an ‘itemID’ of an entity instance. This last one might have multiple entity layouts applied, so we are able to pass in the ID of the entity layout to navigate to the correct one.

Let’s start by creating a new button and return to the (in my case ) default homepage where we opened our ‘Person’ entity from the ‘All persons’ list. Looks like a simple action to me!

The HTML button:

1
<input id="btnNavigate" type="submit" value="Do navigation"/>

The JavaScript part:

1
2
3
4
$("#btnNavigate").click(function() {
let result = window.parent.publicAPIProvider.navigate($.urlParam('homepageIdData'), {});
console.log(result);
});

The only thing with this craftsmanship is that you can’t open the entity instance directly (without opening it via the homepage layout!). Why? Well, because you’ll see the parameter flag homepageIdData is missing in action in the URL of the iFrame! Later on you’ll see these are IDs (also for these homepages) are fixed values for the solution.

After publication, it works as expected! Next one…

Now we’ll try an entity navigation and move to another entity, but for this we need the ‘itemId’ of another entity instance! The simplest way do make this happen is via a “to_one” relation to the (for example) ‘User’ entity. With this relation in place (and available on the ‘Create’ form) it’s possible to make a navigate action like this (because the ‘relatedItemId’ is available from method “publicAPIProvider.getItemData()”):

1
2
3
4
5
6
$("#btnNavigate").click(function() {
window.parent.publicAPIProvider.getItemData($.urlParam('itemid')).then((itemData) => {
let result = window.parent.publicAPIProvider.navigate(itemData.to_one_user.relatedItemId, {});
console.log(result);
});
});

After a publication and the button-click in runtime, we see our ‘User’ entity is opened via the default layout for this entity instance!

api_012

FYI: This “User” entity is part of the ‘Identity package’ of the platform with indeed its own BB implementations and for us to consume from. It looks rather empty, but that’s because I didn’t fill in all the data into OTDS where this user account is consolidated from. Don’t have a clue what I’m talking about? You should have a read here

Let’s add the extra clearBreadcrumb flag in the request with the value ‘true’:

1
let result = window.parent.publicAPIProvider.navigate(itemData.to_one_user.relatedItemId, {'clearBreadcrumb':true});

The difference? Well, have a look (compared with the previous screenshot):

api_013

Finally, I also wanted to play around with jumping from one entity layout to another entity layout for the same ‘Person’ entity. This needs to be done based on a so-called “layoutId”. For our demo I just create a simple layout ID input field on our ‘playground.htm’ which we will update with a valid “layoutId” before we hit our custom ‘Do navigation’ button. Our navigate method will now look like this (where we “stay” at the current entity ‘itemId’, but only switch the layout):

1
2
3
4
$("#btnNavigate").click(function() {
let result = window.parent.publicAPIProvider.navigate($.urlParam('itemid'), {'clearBreadcrumb':true,'breadcrumbName':'Person-1','layoutId':$('#inputLayoutId').val()});
console.log(result);
});

This is my field in the HTML page (just after the other fields):

1
2
3
<div id="formLayoutId" style="padding-bottom: 8px">
<input class="ot-control" id="inputLayoutId" placeholder="LayoutId"/>
</div>

Time to make the switch? Well, not yet as we first need a second layout on our ‘Person’ entity. Just create a second one next to the already available ‘default_layout’. I just made a copy of it and manipulated the panels a bit, so it ‘smells’ different! Now it’s time to get notice of the “layoutId” for those 2 layout items. Have a look here:

api_014

In my case I have 2 values as “layoutId” to play with:

  • 080027e84f06a1ec8ef27e5b70bda744 (my default_layout2 where the action bar is placed in the bottom)
  • 080027e84f06a1ec8df07a5f26bfb1fb (my default_layout where the action bar is placed in the top)

After publication and testing it works like a charm…Nice stuff!

Now the answer to a smart question: Are these ID-values still valid when we package the solution and bring it the next environment? For sure they are!!

api_015

Also notice the breadcrumb! As if you might have seen, we also did a small update with the breadcrumbName parameter in the JavaScript call. Because we stay in the current entity, we clear the breadcrumb and set it to the current value for our demo. If not you would see some breadcrumb like this (after some switches!): Home Page > Person-1 > Person-1 > Person-1


Calling from outside the runtime UI!?

Yes…That was an open question from my side! Is it possible!? Well, as we already learned we need to have access to the JavaScript library coreBundleAwpClient-9e875f5dfe.js. Let’s create a page (playground_external.htm) where we do something like this (where I also include all the JavaScript bundles I see passing by during the opening of the AppWorks runtime):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<link rel="stylesheet" href="../app/start/web/startup.css" type="text/css" />
<script src="/cordys/thirdparty/jquery/jquery.debug.js" type="text/javascript"></script>
<script src="../app/start/web/bundles/coreBundleAwpClient-9e875f5dfe.js" type="text/javascript"></script>
<script src="../app/start/web/bundles/extrasBundleAwpClient-3f10ad1cd4.js" type="text/javascript"></script>
<script src="../app/start/web/thirdpartylibs/csrf.js" type="text/javascript"></script>
<script src="../app/start/web/jspm_packages/system.js" type="text/javascript"></script>
<script src="../app/start/web/config.js" type="text/javascript"></script>
<script src="../app/start/web/worker/wsctf-worker.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
console.log('Hello world...');
publicAPIProvider.updateData('080027e84f06a1ec8deeaa8dab74a43a.1', 'prs_id', 'updated externally!');
});
</script>
</head>
<body>
<h1>New API demo external</h1>
</body>
</html>

An extra note on this action: we are required to first login into the platform before we are able to access this all, but that’s the implementation of a login redirect which is a bit out of scope for this post. For now, we first open the regular platform runtime for a login and after this we update the URL to our external HTML file: http://192.168.56.107:8080/home/appworks_tips/html/playground_external.htm

After some debugging in runtime (and made sure to create a login session through the normal login screen!), I conclude it’s not working this easy? Or maybe it’s just my foolishness…Let me know!? Probably the bundled JavaScript requires something more to initialize/define the publicAPIProvider!?

This is my stack trace from jQuery:

1
2
3
4
"ReferenceError: publicAPIProvider is not defined
at HTMLDocument.<anonymous> (http://192.168.56.107:8080/home/appworks_tips/html/playground_external.htm?itemid=080027e84f06a1ec8deeaa8dab74a43a.1:12:9)
at mightThrow (http://192.168.56.107:8080/cordys/thirdparty/jquery/jquery.debug.js:3762:29)
at process (http://192.168.56.107:8080/cordys/thirdparty/jquery/jquery.debug.js:3830:12)"

Update after a very interesting response from OpenText…Thx again!

We indeed miss some initialization from the AppWorks platform in the external call. To fix this we embed the AppWorks runtime in a ‘hidden’ iFrame, and we wait for it before we execute our custom code. This is the JavaScript part we can use:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$(document).ready(function() {
$("#btnClickMe").click(function() {
console.log('Hello world...');
let intervalID = window.setInterval(() => {
let appworksFrame = document.getElementById('appworks_frame').contentWindow;
if (appworksFrame) {
if (appworksFrame.publicAPIProvider && appworksFrame.publicAPIProvider.updateData) {
let result = appworksFrame.publicAPIProvider.updateData('080027e84f06a1ec8deeaa8dab74a43a.1', 'prs_id', 'updated externally!');
console.log(result);
let result2 = appworksFrame.publicAPIProvider.saveAllItems();
console.log(result2);
clearInterval(intervalID);
console.log('I found the API, Now clear the timer');
}
} else {
console.log('Hook not found, let this timer run again');
}
}, 200);
});
});

And…off-course the button with the ‘hidden’ iFrame:

1
2
<input id="btnClickMe" type="submit" value="Click me"/>
<iframe id='appworks_frame' src="/home/appworks_tips/app/start/web/item/080027e84f06a1ec8deeaa8dab74a43a.1" style="display:none; tabindex:-1; width:100%; height:100%;"></iframe>

Download my ‘playground.htm’ and ‘playground.js’ for your reference. Also have a look at ‘playground_external.htm’


That’s a greatly earned “DONE” with a high valuable ‘Promise’ approach on a tighter customization on our beloved platform. This truly brings hope for the future on the extendibility of AppWorks. It’s also a great replacement for the implementations we currently do with the HTML5SDK, and the scripting in xForm documents. More will definitely be available in the future for as developers to consume; this also depends on the demand, so keep curious on all the new stuff and just try it out and bring feedback to the team for new features. I’m enthusiast, but that’s already clear I hope! 🤗 Have a nice week-end and I CU in the next post.

Final hint: The future will be more brightened with event listeners which makes it possible our customization will listen for events triggered by the platform, so we can react on it! Own man…Will I sleep well tonight!

Don’t forget to subscribe to get updates on the activities happening on this site. Have you noticed the quiz where you find out if you are also “The AppWorks guy”?