Communicate events in ADF based UI areas to embedded Rich Client Applications such as Oracle JET, Angular and React image 16

Communicate events in ADF based UI areas to embedded Rich Client Applications such as Oracle JET, Angular and React

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.

In a previous article, I have described how events in the embedded area are communicated to the ADF side of the fence and lead to UI synchronization: Publish Events from any Web Application in IFRAME to ADF Applications. In the current article, I describe the reverse route: events in the ADF based areas on the page are communicated to the rich client based UI and trigger the appropriate synchronization. The implementation is suitably decoupled, using ADF mechanisms such as server listener, conetxtual event, partial page refresh and the standard HTML5 mechanism for publishing events on embedded IFRAME windows.image_thumb11

 

It is quite likely that an IFRAME is used as a container for the new UI components

The UI components created in ADF and those built in other technologies are fairly well isolated from each other, through the use of the IFRAME. However, in certain instances, the isolation has to be pierced. When a user performs an action in one UI component, it is quite possible that is action should have an effect in another UI area in the same page. The other area may need to refresh (get latest data), synchronize (align with the selection), navigate, etc. We need a solid, decoupled way of taking an event in the ADF based UI area to the UI sections embedded in IFRAMEs and based on one of the proponents of the latest UI technology.

This article describes such an appoach – that allows our ADF side of the User Interface to send events in a well defined way to the JET, React or Angular UI component and thus make these areas play nice with each other after all. The visualization of the end to end flow is shown below:

image

 

Note: where it says JET, it could also say Angular, Vue or React – or even plain HTM5.

The steps are:

  1. A user action is performed and the event to be published is identified in the ADF UI – the ADF X taskflow in the figure.
  2. Several options are available for the communication to the server – from an auto-submit enabled input component with a value change listener associated with a managed bean to a UI comonent with a client listener that leverages a server listener to queue a custom event to be sent to the server – also ending up in a managed bean
  3. The managed bean, defined in the context of the ADF Taskflow X, gets hold of the binding container for the current view, gets hold of the publishEvent method binding and executes that binding
  4. The publishEvent method binding is specified in the page definition for the current page. It invokes method publishEvent on the Data Control EventPublisherBean that was created for the POJO EventPublisherBean. The method binding in the page definition contains an events element that specifies that execution of this method binding will result in the publication of a contextual event called CountrySelectedEvent that takes the result from the method publishEvent as its payload.

    At this point, we leave the ADF X Taskflow. It has done its duty by reporting the event that took place in the client. It is available at the heart of the ADF framework, ready to be processed by one or more consumers – that each have to take care of refreshing their own UI if so desired.

  5. The contextual event CountryChangedEvent is consumed in method handleCountryChangedEvent in POJO EventConsumer. A Data Control is created for this POJO. A method action is configured for handleCountryChangedEvent in the page definition for the view in JET Container ADF Taskflow. This page definition also contains an eventMap element that specifies that the CountryChangedEvent event is to be handled by method binding handleCountryChangedEvent. The method binding specifies a single parameter that will receive the payload of the event (from the EL Expression ${payLoad} for attribute NDValue)
  6. The EventConsumer receives the payload of the event and writes a JavaScript snippet to be executed in the browser at the event of the partial page request processing.
  7. The JavaScript snippet, written by EventConsumer, is executed in the client; it invokes function processCountryChangedEvent (loaded in JS library adf-jet-client-app.js) and pass the payload of the countrychanged event.
  8. Function processCountryChangedEvent locates the IFRAME element that contains the target client application and posts a message on its content window – carrying the event’s payload
  9. A message event handler defined in the IFRAME, in the JET application, consumes the message, extracts the event’s payload and processes it in the appropriate way – probably synchronizing the UI in some way or other.At this point, all effects that the action in ADF X area should have in the JET application in the IFRAME have been achieved.

image

And now for some real code.

Starting point:

  • ADF Web Application (may have Model, such as ADF BC, not necessarily)
    • an index.jsf page – home of the application
    • the ADF JET Container Taskflow with a JETView.jsff that has the embedded IFRAME that loads the index.xhtml
    • a jet-web-app folder with an index.html – to represent the JET application (note: for now it is just a plain HTML5 application)
    • the ADF X Taskflow with a view.jsff page – representing the existing WC Portal or ADF ERP application

 

image_thumb21

 

From ADF X Taskflow to ADF Contextual Event

The page view.jsff contains a selectOneChoice component

image

 

Users can select a country.

The component has autoSubmit set to true – which means that when the selection changes, the change is submitted (in an AJAX request) to the server. The valueChangeListener has been configured – with the detailsBean managed bean, defined in the ADF-X-taskflow.

<?xml version='1.0' encoding='UTF-8'?>
<ui:composition xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:af="http://xmlns.oracle.com/adf/faces/rich">
    <af:panelHeader text="Classic ADF X Taskflow" id="ph1">
        <af:selectOneChoice label="Choose a country" id="soc1" autoSubmit="true"
                            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:selectOneChoice>
    </af:panelHeader>
</ui:composition>

image

The detailsBean is defined for the ADF-X-taskflow:

<?xml version="1.0" encoding="windows-1252" ?>
<adfc-config xmlns="http://xmlns.oracle.com/adf/controller" version="1.2">
  <task-flow-definition id="ADF-X-taskflow">
    <default-activity>view</default-activity>
    <data-control-scope>
      <shared/>
    </data-control-scope>
    <managed-bean id="__1">
      <managed-bean-name>detailsBean</managed-bean-name>
      <managed-bean-class>nl.amis.frontend.jet2adf.view.adfX.DetailsBean</managed-bean-class>
      <managed-bean-scope>pageFlow</managed-bean-scope>
    </managed-bean>
    <view id="view">
      <page>/view.jsff</page>
    </view>
    <use-page-fragments/>
  </task-flow-definition>
</adfc-config>

The bean is based on class DetailsBean. The relevant method here is countryChangedHandler:

    public void countryChangeHandler(ValueChangeEvent valueChangeEvent) {
        System.out.println("Country Changed to = " + valueChangeEvent.getNewValue());
        // find operation binding publishEvent and execute in order to publish contextual event
        BindingContainer bindingContainer = BindingContext.getCurrent().getCurrentBindingsEntry();
        OperationBinding method = bindingContainer.getOperationBinding("publishEvent");
        method.getParamsMap().put("payload", valueChangeEvent.getNewValue());
        method.execute();
    }

CODE details bean country changed handler

This method gets hold of the binding container (for the current page, view.jsff) and in it of the method action publishEvent

<?xml version="1.0" encoding="UTF-8" ?>
<pageDefinition xmlns="http://xmlns.oracle.com/adfm/uimodel" version="12.2.1.9.14" id="viewPageDef"
                Package="nl.amis.frontend.jet2adf.view.pageDefs">
    <parameters/>
    <executables>
        <variableIterator id="variables"/>
    </executables>
    <bindings>
        <methodAction id="publishEvent" RequiresUpdateModel="true" Action="invokeMethod" MethodName="publishEvent"
                      IsViewObjectMethod="false" DataControl="EventPublisherBean"
                      InstanceName="bindings.publishEvent.dataControl.dataProvider"
                      ReturnName="data.EventPublisherBean.methodResults.publishEvent_publishEvent_dataControl_dataProvider_publishEvent_result">
            <NamedData NDName="payload" NDType="java.lang.Object"/>
            <events xmlns="http://xmlns.oracle.com/adfm/contextualEvent">
                <event name="CountryChangedEvent"/>
            </events>
        </methodAction>
    </bindings>
</pageDefinition>

This Page Definition defines the method action and specifies that execution of that method action publishes the Contextual Event CountryChangedEvent.

 

At this point, we leave the ADF X Taskflow. It has done its duty by reporting
the event that took place in the client. It is available at the heart of the ADF
framework, ready to be processed by one or more consumers – that each have to
take care of refreshing their own UI if so desired.

 

From ADF Contextual Event to JET Application

A method action is configured for method
handleCountryChangedEvent in data control EventConsumer created for POJO EventConsumer, in the page definition for the view in JET
Container ADF Taskflow. This page definition also contains an eventMap
element that specifies that the CountryChangedEvent event is to be handled by this method binding handleCountryChangedEvent. The method binding specifies
a single parameter that will receive the payload of the event (from the EL
Expression ${payLoad} for attribute NDValue)

Here is the code for the Page Definition for the JETView.jsff:

<?xml version="1.0" encoding="UTF-8" ?>
<pageDefinition xmlns="http://xmlns.oracle.com/adfm/uimodel" version="12.2.1.9.14" id="JETViewPageDef"
                Package="nl.amis.frontend.jet2adf.view.pageDefs">
    <parameters/>
    <executables>
        <variableIterator id="variables"/>
    </executables>
    <bindings>
         <methodAction id="handleCountryChangedEvent" RequiresUpdateModel="true" Action="invokeMethod"
                      MethodName="handleCountryChangedEvent" IsViewObjectMethod="false" DataControl="EventConsumer"
                      InstanceName="bindings.handleCountryChangedEvent.dataControl.dataProvider">
            <NamedData NDName="payload" NDValue="${payLoad}" NDType="java.lang.Object"/>
        </methodAction>
    </bindings>
    <eventMap xmlns="http://xmlns.oracle.com/adfm/contextualEvent">
        <event name="CountryChangedEvent">
            <producer region="*">
            <!-- http://www.jobinesh.com/2014/05/revisiting-contextual-event-dynamic.html -->
                <consumer handler="handleCountryChangedEvent" refresh="false"/>
            </producer>
        </event>
    </eventMap>
</pageDefinition>

Note: the refresh attribute in the consumer element is crucial: it specifies that the page should not be refreshed when the event is consumed. The default is that the page does refresh; that would mean in our case that the IFRAME refreshes and reloads the JET application that is reinitialized and loses all it state.

And here is the EventConsumer class – for which a Data Control has been created – that handles the CountryChangedEvent:

package nl.amis.frontend.jet2adf.view.adfjetclient;

import javax.faces.context.FacesContext;

import org.apache.myfaces.trinidad.render.ExtendedRenderKitService;
import org.apache.myfaces.trinidad.util.Service;

public class EventConsumer {
    public EventConsumer() {
        super();
    }

    public void handleCountryChangedEvent(Object payload) {
        System.out.println(">>>>>> Consume Event: " + payload);
        writeJavaScriptToClient("console.log('CountryChangeEvent was consumed; the new country value = "+payload+"'); processCountryChangedEvent('"+payload+"');");
      }

    //generic, reusable helper method to call JavaScript on a client
    private void writeJavaScriptToClient(String script) {
        FacesContext fctx = FacesContext.getCurrentInstance();
        ExtendedRenderKitService erks = null;
        erks = Service.getRenderKitService(fctx, ExtendedRenderKitService.class);
        erks.addScript(fctx, script);
    }
}

The contextual event CountryChangedEvent is consumed in method handleCountryChangedEvent in this POJO EventConsumer. It receives the payload of the event and writes a
JavaScript snippet to be executed in the browser at the event of the partial
page request processing, using the ExtendedRenderKitService in the ADF framework.

The JavaScript snippet, written by EventConsumer:

  console.log('CountryChangeEvent was consumed; the new country value = uk'); 
  processCountryChangedEvent('uk');

It is executed in the client;
it invokes function processCountryChangedEvent (loaded in JS library
adf-jet-client-app.js) and passes the payload of the countrychanged event (that is: the country code for the selected country).

Function processCountryChangedEvent locates the IFRAME element that
contains the target client application and posts a message on its content
window – carrying the event’s payload:

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 iframe = findIframeWithIdEndingWith('jetIframe::f');
    var targetOrigin = '*';
    iframe.contentWindow.postMessage({'eventType':'countryChanged','payload':newCountry}, targetOrigin);
}

A message event handler defined in the IFRAME, in the JET application,
consumes the message, extracts the event’s payload and processes it.

          function init() {
              // 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 &amp;&amp; event.data.eventType == 'countryChanged' &amp;&amp; event.data.payload) {
                      var countrySpan = document.getElementById('currentCountry');
                      countrySpan.innerHTML = "Fresh Country: " + event.data.payload;
                  }
              },
              false);
          }
          //init
          document.addEventListener("DOMContentLoaded", function (event) {
              init();
          });

It receives the event, gets the data property that contains the payload (as a String) and parses it as JSON (to turn it into a JavaScript object). It extracts the country from the event. Then it locates a SPAN element in the DOM and updates its innerHTML property. This will update the UI:

image

Here the salient details of the index.html of the embedded web application:

    <body>
        <h2>Client Web App</h2>
        <p>
            Country is 
            <span id="currentCountry"></span>
        </p>
    </body>

The full project looks like this:

image

 

A simple animated visualization of what happens:

Webp.net-gifmaker (4)

Resources

Sources for this article: https://github.com/lucasjellema/WebAppIframe2ADFSynchronize. (note: this repository also contains the code for the flow from JET IFRAME to the ADF Taskflow X

Docs on postMessage: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

Blog ADF: (re-)Introducing Contextual Events in several simple steps  : https://technology.amis.nl/2013/03/14/adf-re-introducing-contextual-events-in-several-simple-steps/

Blog Revisiting Contextual Event : Dynamic Event Producer, Manual Region Refresh, Conditional Event Subscription and using Managed Bean as Event Handler (with the crucial hint regarding supressing the automatic refresh of pages after consuming a contextual event

3 Comments

  1. Amar February 19, 2019
    • Lucas Jellema February 19, 2019
      • Amar February 19, 2019