Even though ADF does not always play nice with guests, it is becoming increasingly common to have ADF applications – pure ADF or WebCenter Portal – that are the host to embedded user interfaces, created in technologies such as Oracle JET, Vue.js, Angular or React. 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.
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 embedded IFRAME area of the latest UI technology and bring it all the way to the other (regular) ADF taskflows in the page.
This article describes such an appoach – that allows our JET, React or Angular UI component to send events in a well defined way to the ADF side of the User Interface and thus make these areas play nice with each other after all. The visualization of the end to end flow is shown below:
Note: where it says JET, it could also say Angular, Vue or React – or even plain HTM5.
The steps are:
- The event to be published is identified in the next gen UI, client side in JavaScript. A JavaScript function is invoked. This function uses the postMessage method on the parent window (the one that contains the IFRAME) to send an event to the enclosing page (see for example: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
- The ADF JET Container Taskflow that embeds the IFRAME references a JavaScript library that attaches an event listener to the window, to listen for messages posted from within the IFRAME. When a message is received, this library locates the <af:inlineFrame> component, and queues a custom event on its serverListener element
- The custom event is sent asynchronously (AJAX style) from the browser to a managed bean, defined in the context of the JET Container Taskflow. This managed bean gets hold of the binding container for the current view in the ADF JET Container Taskflow, gets hold of the publishEvent method binding and executes that binding
- 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 ClientAppEvent that takes the result from the method publishEvent as its payload.
At this point, we leave the ADF JET Container 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.
- The contextual event ClientAppEvent is consumed in method handleEvent in POJO EventHandlerBean. A Data Control is created for this POJO. A method action is configured for handleEvent in the page definition for the view in ADF Taskflow X. This page definition also contains an eventMap element that specifies that the ClientAppEvent event is to be handled by method binding handleEvent. The method binding specifies a single parameter that will receive the payload of the event (from the EL Expression ${payLoad} for attribute NDValue)
- The EventHandlerBean receives the payload of the event and enlists the managed detailsBean to do the actual work for this event
- DetailsBean will derive relevant data from the payload, update bean properties, add UI components as partial targets (to be refreshed in the client) and adds some JavaScript to be executed in the browser once the request completes – the request that started with the custom event queued via the serverListener in response to the postMessage from within the IFRAME
- Upon completing the request, ADF will refresh the partial targets in the browser and will execute the JavaScript provided by DetailsBean.
At this point, all effects that the action in the IFRAME should have in the ADF Application – both server side as well as client side – have been achieved.
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
Adding:
JavaScript function callParent in index.html; this function is invoked to post a message to the parent window.
function callParent() { console.log('send message from Web App to parent window'); var jetinputfield = document.getElementById('jetinputfield'); var inputvalue = jetinputfield.value; var message = { "message" : { "value" : inputvalue }, "mydata" : { "param1" : 42, "param2" : "train" } }; // 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); }
JavaScript library adf-jet-client-app.js, associated with JETView.jsff – registers a message listener on the window and handles incoming events (by queueing a custom event on server listener)
var jetIframeClientId =""; function init() { window.addEventListener("message", function (event) { console.log("Parent receives message from iframe " + event); sendMessageFromJetToServer(event.data); }, false); // jetIframe.contentWindow.postMessage("hello tyhere", '*'); } document.addEventListener("DOMContentLoaded", function (event) { init(); }); function sendMessageFromJetToServer(message) { var jetIframeADF = AdfPage.PAGE.findComponentByAbsoluteId(jetIframeClientId); AdfCustomEvent.queue(jetIframeADF, "messageRouter", message, true); }
The page JETView.jsff contains the inlineFrame element that has a serverListener that provides the connection to the server – managed bean client2serverBean
<?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" xmlns:f="http://java.sun.com/jsf/core"> <af:resource type="javascript" source="resources/js/adf-jet-client-app.js"/> <af:panelStretchLayout id="psl1" styleClass="AFStretchWidth" bottomHeight="0px" topHeight="40px" endWidth="0px" startWidth="0px" dimensionsFrom="parent"> <f:facet name="top"> <af:panelHeader text="JET Client" id="ph1"> <!-- we do not really care about this client attribute; however, by rendering it we force execution of method getJetInlineFrameClientId() on Client2Server that also writes a little snippet of JavaScript to set the JavaScript variable jetIframeClientId with the actual client side id for the iframe --> <af:clientAttribute name="inlineFrameId" value="#{client2serverBean.jetInlineFrameClientId}"/> </af:panelHeader> </f:facet> <f:facet name="center"> <af:panelGroupLayout id="pg1"> <af:inlineFrame source="jet-web-app/index.xhtml?token=yuweyuweyu" id="jetIframe" sizing="preferred" styleClass="AFStretchWidth" binding="#{client2serverBean.jetIframe}"> <af:serverListener type="messageRouter" method="#{client2serverBean.handleMessageFromJet}"/> </af:inlineFrame> </af:panelGroupLayout> </f:facet> </af:panelStretchLayout> </ui:composition>
Note the binding attribute on inlineFrame that ties this component to the jetIframe property on the managed bean. We use this component binding in Client2Server to extract the real client id for the IFrame component and make that available in JavaScript; this happens in method getJetInlineFrameClientId() that is invoked when rendering the (utterly useless) clientAttribute inlineFrameId on the panelHeader (we only create this clientAttribute in order to force execution of method getJetInlineFrameClientId().
Java Class Client2Server – handles server side of server listener: processing incoming custom events from client, i.e. JETView.jsff.
package nl.amis.frontend.jet2adf.view.adfjetclient; import javax.faces.context.FacesContext; import oracle.adf.model.BindingContext; import oracle.adf.view.rich.component.rich.output.RichInlineFrame; import oracle.adf.view.rich.render.ClientEvent; import oracle.binding.BindingContainer; import oracle.binding.OperationBinding; import org.apache.myfaces.trinidad.render.ExtendedRenderKitService; import org.apache.myfaces.trinidad.util.ComponentReference; import org.apache.myfaces.trinidad.util.Service; public class Client2Server { public Client2Server() { super(); } public String getJetInlineFrameClientId() { FacesContext ctx = FacesContext.getCurrentInstance(); String id = this.getJetIframe().getClientId(ctx); writeJavaScriptToClient("console.log('Inline Frame Id = "+id+"'); jetIframeClientId ='"+id+"'"); return id; } //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); } // this method is called from the serverListener in client-to-server.jsf; it receives an event with a payload that contains a key helpTopic public void handleMessageFromJet(ClientEvent clientEvent) { System.out.println("handleMessageFromJet in Server!" + clientEvent); String message = clientEvent.getParameters() .get("message") .toString(); System.out.println("String from JET = " + message); // 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", message); method.execute(); } private ComponentReference jetInlineFrame; public void setJetIframe(RichInlineFrame jetIframe) { this.jetInlineFrame = ComponentReference.newUIComponentReference(jetIframe); } public RichInlineFrame getJetIframe() { if (this.jetInlineFrame != null) { return (RichInlineFrame) this.jetInlineFrame.getComponent(); } else { return null; } } }
configured as Managed Bean client client2serverBean in ADF JET Container 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-JET-Container-taskflow"> <default-activity>JETView</default-activity> <data-control-scope> <shared/> </data-control-scope> <managed-bean id="__1"> <managed-bean-name>client2serverBean</managed-bean-name> <managed-bean-class>nl.amis.frontend.jet2adf.view.adfjetclient.Client2Server</managed-bean-class> <managed-bean-scope>request</managed-bean-scope> </managed-bean> <view id="JETView"> <page>/JETView.jsff</page> </view> <use-page-fragments/> </task-flow-definition> </adfc-config>
Java Class EventPublisherBean – a simple POJO that will publish the contextual event – by virtue of being a data control and having its method configured in the page definition as method action with contextual event effect ClientAppEvent
package nl.amis.frontend.jet2adf.view.adfjetclient; public class EventPublisherBean { public EventPublisherBean() { super(); } public Object publishEvent(Object payload) { System.out.println("<<< Publish Event: "+payload); return payload; } }
Create Data Control for this Java Class.
Create the Page Definition for JETViewPageDef. Create method action for EventPublisherBean.publishEvent with event element for ClientAppEvent in JETViewPageDef (page definition for 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="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="ClientAppEvent"/> </events> </methodAction> </bindings> </pageDefinition>
Consume Event and Update ADF X Client
The ClientAppEvent should be consumed in TaskFlow ADF-X-taskflow’s view.jsff page. This done by defining an event-map entry for that event in the page definition for view.jsff:
<?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="handleEvent" RequiresUpdateModel="true" Action="invokeMethod" MethodName="handleEvent" IsViewObjectMethod="false" DataControl="EventHandlerBean" InstanceName="bindings.handleEvent.dataControl.dataProvider"> <NamedData NDName="payload" NDValue="${payLoad}" NDType="java.lang.Object"/> </methodAction> </bindings> <eventMap xmlns="http://xmlns.oracle.com/adfm/contextualEvent"> <event name="ClientAppEvent"> <producer region="*"> <consumer handler="handleEvent"/> </producer> </event> </eventMap> </pageDefinition>
The consumer for the event references the handleEvent method action that is created for the handleEvent method in the EventHandlerBean POJO for which a data control is created.
package nl.amis.frontend.jet2adf.view.adfX; import javax.el.ELContext; import javax.el.ExpressionFactory; import javax.el.ValueExpression; import javax.faces.context.FacesContext; public class EventHandlerBean { public EventHandlerBean() { super(); } public void handleEvent(Object payload) { System.out.println(">>>>>> Consume Event: " + payload); DetailsBean db = (DetailsBean)evaluateEL("#{pageFlowScope.detailsBean}"); db.process(payload); } public static Object evaluateEL(String el) { FacesContext fc = FacesContext.getCurrentInstance(); ELContext elContext = fc.getELContext(); ExpressionFactory ef = fc.getApplication().getExpressionFactory(); ValueExpression exp = ef.createValueExpression(elContext, el, Object.class); Object obj = exp.getValue(elContext); return obj; } }
CODE eventhandlerbean
The EventHandlerBean leverages the DetailsBean – a POJO that is setup as pageflow scope managed bean in 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 POJO DetailsBean:
package nl.amis.frontend.jet2adf.view.adfX; import javax.el.ELContext; import javax.el.ExpressionFactory; import javax.el.ValueExpression; import javax.faces.context.FacesContext; public class EventHandlerBean { public EventHandlerBean() { super(); } public void handleEvent(Object payload) { System.out.println(">>>>>> Consume Event: " + payload); DetailsBean db = (DetailsBean)evaluateEL("#{pageFlowScope.detailsBean}"); db.process(payload); } public static Object evaluateEL(String el) { FacesContext fc = FacesContext.getCurrentInstance(); ELContext elContext = fc.getELContext(); ExpressionFactory ef = fc.getApplication().getExpressionFactory(); ValueExpression exp = ef.createValueExpression(elContext, el, Object.class); Object obj = exp.getValue(elContext); return obj; } }
This bean’s process method is called by the EventHandlerBean when it consumes the ClientAppEvent. This method does several things:
- it updates the message property (that provides the value for the inputText comonent)
- it adds the inputText component as partial target to be updated in the client
- it writes a JavaScript snippet to be executed in the client (that writes a simple line of logging to the console – and potentially could do all kinds of wild stuff)
The view.jsff file defines the inputText element that is bound to the detailsBean and also takes its value from that bean. This component will be updated in client when the event consumption is completed.
<?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:inputText label="Message" id="it1" binding="#{pageFlowScope.detailsBean.messageComponent}" value="#{pageFlowScope.detailsBean.message}" columns="120" rows="1"/> </af:panelHeader> </ui:composition>
The full project:
And the event exchange in action:
Resources
Sources for this article: https://github.com/lucasjellema/WebAppIframe2ADFSynchronize.
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/