For one of our current projects I have done some explorations into the combination of ADF (and WebCenter Portal in our specific case) with JET. Our customer has existing investments in WC Portal and many ADF Taskflows and is now switching to JET as a WebApp implementation technology – for reasons of better user experience and especially better availability of developers. I believe that this is a situation that many organizations are in or are contemplating (including those who want to extend Oracle EBusiness Suite or Fusion Apps). This is not the ideal green field technology mix of course. However, if either WebCenter Portal (heavily steeped in ADF) or an existing enterprise ADF application are the starting point for new UI requirements, you are bound to end up with a combination of ADF and the latest and greatest technology used for building those requirements.
We have to ensure that the rich client based ‘Portlets’ are nicely embedded in the ADF host environment. We also have to take care that events triggered by user actions in the ADF UI areas are communicated to the embedded rich client based UI areas in the page and lead to appropriate actions over there – and the same for actions in the embedded UI areas and events flowing in the other direction.
In two previous articles (Publish Events from any Web Application in IFRAME to ADF Applications and Communicate events in ADF based UI areas to embedded Rich Client Applications such as Oracle JET, Angular and React), I have described how events in the embedded area are communicated to the ADF side of the fence and lead to UI synchronization and similary how events in the traditional ADF based UIs are communicated to the embedded areas and trigger the appropriate synchronization. The implementation described in these articles is based on pure, native ADF mechanisms such as server listener, contextual event, partial page refresh in combination with standard HTML5 mechanism for publishing events on embedded IFRAME windows. The route described using these out of the box mechanisms is robust, proven and very decoupled. It allows run time configuration in WebCenter Portal (wiring of taskflows leveraging the contextual event). This route is also somewhat heavyhanded; it is not very fast – dependencing on network latency to the backend – and it puts additional load on the application server.
There is a fast, light-weight alternative to the use of contextual (server side) events for communication between areas in an ADF based web page. One that can help with interaction between ADF based areas and non-ADF areas (JET, React, Angular) – but also with interactions between two or more pure ADF areas. An alternative that I believe should be part of the native ADF framework – but is not. This alternative is: the client side event bus.
The client side event bus is a very simple pure JavaScript client side component – that I have introduced in an earlier article. In essence, this is what it is:
The client side event bus is loaded in the outermost page and will be available throughout the lifetime of the application. It has a registry of event subscriptions that each consist of the name of the event type and a function reference to the function that should be called to handle the event. Each UI area produced from an ADF Taskflow can contain JavaScript snippets that create event handlers (JavaScript functions) and subscribe those with the event bus for a specific event type. Finally, each UI area can publish an event to the event bus whenever something happens that is worth publishing. Of course this is somewhat loosely stated – we should document with some rigor the client side events that each UI area will publish – and will consume – just like the contextual (server side) events with taskflows. It is my recommendation that for the ADF application as a whole, an event registry is maintained that describes all events that can be published – client side or contextual server side – along with the payload for each event.
Let’s make use of this client side event bus for the following use case:
The ADF application embeds a client side web application in an IFRAME in an ADF Taskflow – ADF-JET-Container-taskflow. The application contains a second taskflow – ADF-X-taskflow – that is pure ADF, no embedding whatsoever. The challenge: an event taking place in the client side UI area produced from ADF-X-taskflow should have an effect in the client side web application – plain HTML5 or Oracle JET – in the IFRAME in the UI area produced from the other ADF Taskflow, and we want this effect to be produced as quickly and smoothly as possible and given the nature of the event and the effect there is no need for server side involvement. In this case, using contextual events is almost wasteful – it is not simple to implement, it is not efficient or fast to execute and it does not buy us anything in terms of additional security, scalability or functionality. So let’s use this client side event bus.
The steps to implement – on top of the ADF application with the index.jsf page, the two ADF Taskflows with their respective views and the embedded IFRAME plus web application – are as follows:
(note: all code can be found on GitHub: https://github.com/lucasjellema/WebAppIframe2ADFSynchronize/releases/tag/v3.0)
1. Create JavaScript library adf-client-event-bus.js with the functionality to record subscriptions and forward published events to the event handlers for the specific event types
var subscriptions = {}; function publishEvent( eventType, payload) { console.log('Event published of type '+eventType); console.log('Event payload'+JSON.stringify(payload)); // find all subscriptions for this event type if (subscriptions[eventType]) { // loop over subscriptions and invoke callback function for each subscription for (i = 0; i < subscriptions[eventType].length; i++) { var callback = subscriptions[eventType][i]; try { callback(payload); } catch (err) { console.log("Error in calling callback function to handle event. Error: "+err.message); } }//for }//if }// publishEvent // register an interest in an eventType by providing a callback function that takes a payload parameter function subscribeToEvent( eventType, callback) { if (!subscriptions[eventType]) { subscriptions[eventType]= [ ]}; subscriptions[eventType].push(callback); console.log('added subscription for eventtype '+eventType); }//subscribeToEvent
2. Add adf-client-event-bus.js to the main index.jsf page.
<af:resource type="javascript" source="/resources/js/adf-client-event-bus.js"/>
3. Add client listener to the input component on which the event of interest takes place. In this case: a selectOneChoice from which the user selects a country in view.jsff in taskflow ADF-X-taskflow;
<af:selectOneChoice label="Choose a country" id="soc1" autoSubmit="false" valueChangeListener="#{pageFlowScope.detailsBean.countryChangeHandler}"> <af:selectItem label="The Netherlands" value="nl" id="si1"/> <af:selectItem label="Germany" value="de" id="si2"/> <af:selectItem label="United Kingdom of Great Brittain and Northern Ireland" value="uk" id="si3"/> <af:selectItem label="United States of America" value="us" id="si4"/> <af:selectItem label="Spain" value="es" id="si5"/> <af:selectItem label="Norway" value="no" id="si6"/> <af:clientListener method="countrySelectionListener" type="valueChange"/> </af:selectOneChoice>
the client listener is configured to invoke a client side JavaScript function countrySelectionListener
4. Add function countrySelectionListener to the adf-x-taskflow-client.js JavaScript library that is associated with the page(s) in the ADF X taskflow; this function publishes the client side event countrySelectionEvent
function countrySelectionListener(event) { var selectOneChoice = event.getSource(); var newValue = selectOneChoice.getSubmittedValue(); var selectItems= selectOneChoice.getSelectItems(); var selectedItem = selectItems[newValue]; publishEvent("countrySelectionEvent", { "selectedCountry" : selectedItem._label, "sourceTaskFlow" : "ADF-X-taskflow" }); }
5. Add function handleCountrySelection to the adf-jet-client-app.js JavaScript library that is associated with the JETView.jsff container page in the ADF-JET-Container-taskflow; this function will handle the client event countrySelectionEvent by posting an event message to the IFRAME that contains the client side web application. Also add the call to subscribe this function with the client event bus for events of this type:
subscribeToEvent("countrySelectionEvent", handleCountrySelection); function handleCountrySelection(payload) { var country= payload.selectedCountry; var message = { 'eventType' : 'countryChanged', 'payload' : country }; postMessageToJETIframe(message); } //handleCountrySelection
6. Add JavaScript code in view.xhtml in the client wide web app to process an incoming message event of type countryChanged. This event will trigger an update in the UI.
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <title>Client Side Web App</title> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%3E%0A%20%20%20%20%20%20%20%20%20%20function%20init()%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20attach%20listener%20to%20receive%20message%20from%20parent%3B%20this%20is%20not%20required%20for%20sending%20messages%20to%20the%20parent%20window%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20window.addEventListener(%22message%22%2C%20function%20(event)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20console.log(%22Iframe%20receives%20message%20from%20parent%22%20%2B%20event.data)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(event.data%20%26amp%3B%26amp%3B%20event.data.eventType%20%3D%3D%20'countryChanged'%20%26amp%3B%26amp%3B%20event.data.payload)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20var%20countrySpan%20%3D%20document.getElementById('currentCountry')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20countrySpan.innerHTML%20%3D%20%22Fresh%20Country%3A%20%22%20%2B%20event.data.payload%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20false)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%2F%2Finit%0A%20%20%20%20%20%20%20%20%20%20document.addEventListener(%22DOMContentLoaded%22%2C%20function%20(event)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20init()%3B%0A%20%20%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%20%20%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" /> </head> <body> <h2>Client Web App</h2> Country is <span id="currentCountry"></span> </body> </html>
When the user selects a country using the dropdownlist in area ADF X, the selected country name is displayed almost instantaneously in the IFRAME area based on the rich client web application.
Client Side Event Flow from Embedded Web Application (in IFRAME) to ADF powered Area
Our story would not be complete if we did not also discuss the flow from the embedded UI area to the ADF based UI. It is very similar of course to what we described above. The event originates in the web application and is communicated from within the IFRAME to the parent window and handled by a JavaScript handler loaded for the ADF JET Container Taskflow. This handler publishes a client event with the client side event bus. In this case, a subscription for this event was created from the adf-x-taskflow-client.js library, subscribing a handler function handleDeepMessageSelection that updates the client side message component.
The details steps and code snippets:
1. Add code in view.xhtml to publish a message to the parent window with the message entered by a user in the text field
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <title>Client Side Web App</title> <!-- <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20src%3D%22client-web-app-lib.js%22%3E%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" />--> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%3E%0A%20%20%20%20%20%20%20%20%20%20function%20callParent()%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20console.log('send%20message%20from%20Web%20App%20to%20parent%20window')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20var%20jetinputfield%20%3D%20document.getElementById('jetinputfield')%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20var%20inputvalue%20%3D%20jetinputfield.value%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20var%20message%20%3D%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22message%22%20%3A%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22value%22%20%3A%20inputvalue%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2C%22eventType%22%20%3A%20%22deepMessage%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22mydata%22%20%3A%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22param1%22%20%3A%2042%2C%20%22param2%22%20%3A%20%22train%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20here%20we%20can%20restrict%20which%20parent%20page%20can%20receive%20our%20message%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20by%20specifying%20the%20origin%20that%20this%20page%20should%20have%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20var%20targetOrigin%20%3D%20'*'%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20parent.postMessage(message%2C%20targetOrigin)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%2F%2FcallParent%0A%20%20%20%20%20%20%20%20%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" /> </head> <body> <h2>Client Web App</h2> <input id="jetinputfield" type="text" value="Default"/> <a href="#" onclick="callParent()">Send Message</a> </body> </html>
2. Attach message event listener in the JavaScript library adf-jet-client-app.js for the message events (for example from the embedded IFRAME); in this handler, publish the event as a deepMessageEvent on the client side event bus
function init() { window.addEventListener("message", function (event) { console.log("Parent receives message from iframe " + JSON.stringify(event.data)); var data = event.data; var message = data["message"]; if (data && message){ if ( message['eventType'] == 'deepMessage') { console.log("ADF JET Container Taskflow received deep message event from web App") var message = message.value; publishEvent("deepMessageEvent", { "message" : message ,"sourceTaskFlow" :"ADF-JET-container-taskflow" ,"eventOrigin" : "JET:jet-embedded" }); } } }, false); } document.addEventListener("DOMContentLoaded", function (event) { init(); });
3. From the adf-x-taskflow-client.js library, subscribe a function as event handler for the deepMessageEvent with the client side event bus
subscribeToEvent("deepMessageEvent", handleDeepMessageSelection); function handleDeepMessageSelection(payload) { console.log("DeepMessageEvent consumed in ADF X Taskflow" + JSON.stringify(payload)); var message = payload.message; // find inputText component using its fake styleClass: messageInputHandle // <af:inputText label="Message" id="it1" columns="120" rows="1" styleClass="messageInputHandle"/> var msgInputFieldId = document.getElementsByClassName("messageInputHandle")[0].id; var msgInputText = AdfPage.PAGE.findComponentByAbsoluteId(msgInputFieldId); msgInputText.setValue(message); }
This function extracts the value of the message and sets an inputText component with that value – on the client
The end to end flow looks like this:
Client Side Interaction with JET application
The interaction as described above with a plain HTM5 web application embedded in an ADF application is not any different when the embedded application is an Oracle JET application. This figure shows an example of a JET application embedded in the JET Client area. It consumes two client side events from the ADF parent environment: countrySelection and colorSelection. It publishes an event itself: browserSelectionEvent. All interaction around these events with the client side event bus is taken care of by the ADF JET Container Taskflow. All interaction between the JET application and the ADF JET Container Taskflow is handled through the postMessage mechanism on the IFRAME’s content window and its parent window.
The salient code snippets in the JET application are:
The ViewModel:
define( ['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojknockout', 'ojs/ojinputtext', 'ojs/ojselectcombobox' ], function (oj, ko, $) { 'use strict'; function WorkareaViewModel() { var self = this; // initialize two country observables self.country = ko.observable("Italy"); self.color = ko.observable("Greenish"); self.browser = ko.observable("Chrome"); self.callParent = function (message) { console.log('send message from Web App to parent window'); // here we can restrict which parent page can receive our message // by specifying the origin that this page should have var targetOrigin = '*'; parent.postMessage(message, targetOrigin); } self.browserChangedListener = function (event) { var newBrowser = event.detail.value; var oldBrowser = event.detail.previousValue; console.log("browser changed to:" + newBrowser); var message = { "message": { "eventType": "browserChanged", "value": newBrowser } }; self.callParent(message); } self.init = function () { // attach listener to receive message from parent; this is not required for sending messages to the parent window window.addEventListener("message", function (event) { console.log("Iframe receives message from parent" + event.data); if (event.data && event.data.eventType == 'countryChanged' && event.data.payload) { self.country(event.data.payload); } if (event.data && event.data.eventType == 'colorChanged' && event.data.payload) { self.color(event.data.payload); } } //init $(document).ready(function () { self.init(); }) } return new WorkareaViewModel(); } );
The View:
<h2>Workarea</h2> <div> <oj-label for="country-input">Country</oj-label> <oj-input-text id="country-input" value="{{country}}" ></oj-input-text> <h4 data-bind="text: country"></h4> <oj-label for="color-input">Color</oj-label> <oj-input-text id="color-input" value="{{color}}"></oj-input-text> <h4 data-bind="text: color"></h4> <oj-label for="combobox">Browser Type Selection</oj-label> <oj-combobox-one id="combobox" value="{{browser}}" on-value-changed="{{browserChangedListener}}" style="max-width:20em"> <oj-option value="Internet Explorer">Internet Explorer</oj-option> <oj-option value="Firefox">Firefox</oj-option> <oj-option value="Chrome">Chrome</oj-option> <oj-option value="Opera">Opera</oj-option> <oj-option value="Safari">Safari</oj-option> </oj-combobox-one> </div>
The corresponding code in the ADF JET client app library:
var jetIframeClientId = ""; function init() { window.addEventListener("message", function (event) { console.log("Parent receives message from iframe " + JSON.stringify(event.data)); var data = event.data; var message = data["message"]; if (data && message){ if ( message['eventType'] == 'browserChanged') { console.log("ADF JET Container Taskflow received browser changed event from JET App") var browser = message.value; publishEvent("browserSelectionEvent", { "selectedBrowser" : browser ,"sourceTaskFlow" :"ADF-JET-container-taskflow" ,"eventOrigin" : "JET:jet-embedded" }); } }, false); } document.addEventListener("DOMContentLoaded", function (event) { init(); }); function findIframeWithIdEndingWith(idEndString) { var iframe; var iframeHtmlCollectionArray = document.getElementsByTagName("iframe"); //http://clubmate.fi/the-intuitive-and-powerful-foreach-loop-in-javascript/#Looping_HTMLCollection_or_a_nodeList_with_forEach [].forEach.call(iframeHtmlCollectionArray, function (el, i) { if (el.id.endsWith(idEndString)) { iframe = el; } }); return iframe; } function processCountryChangedEvent(newCountry) { console.log("Client Side handling of Country Changed event; now transfer to IFRAME"); var message = { 'eventType' : 'countryChanged', 'payload' : newCountry }; postMessageToJETIframe(message); } function postMessageToJETIframe(message) { var iframe = findIframeWithIdEndingWith('jetIframe::f'); var targetOrigin = '*'; iframe.contentWindow.postMessage(message, targetOrigin); } subscribeToEvent("colorSelectionEvent", handleColorSelection); function handleColorSelection(payload) { console.log("ColorSelectionEvent consumed " + JSON.stringify(payload)); var color = payload.selectedColor; console.log("selected color " + color); var message = { 'eventType' : 'colorChanged', 'payload' : color }; postMessageToJETIframe(message); } //handleColorSelection subscribeToEvent("countrySelectionEvent", handleCountrySelection); function handleCountrySelection(payload) { var country= payload.selectedCountry; var message = { 'eventType' : 'countryChanged', 'payload' : country }; postMessageToJETIframe(message); } //handleCountrySelection
Resources
Sources for this article: https://github.com/lucasjellema/WebAppIframe2ADFSynchronize. (note: this repository also contains the code for the flows from and to JET IFRAME to and from the ADF Taskflow X via the server side – the traditional ADF approach
Blog Client Side Event Bus in Rich ADF Web Applications – for easier, faster decoupled interaction across regions : https://technology.amis.nl/2017/01/11/client-side-event-bus-in-rich-adf-web-applications-for-easier-faster-decoupled-interaction-across-regions/
Docs on postMessage: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage