Friday, March 15, 2013

ADF Faces Client Behavior with Attributes

Last month I posted about the basics of creating custom ClientBehavior in ADF Faces. Now it is time to take the next step and enhance that example with ways to set attributes on the JSP client-behavior tag, pass these on to the client-side javascript implementation and use them in the actual functionality.

We'll take the (simplified) example of the previous post that could show a javascript alert and adopt it to get the message from an attribute which we specify in the JSF page using the component. We'll also make sure we can use an EL expression to specify this values (this is the tricky part). We will be able to do something like:
<af:forEach begin="1" end="5" varStatus="vs">
  <af:commandButton text="click to see message #{vs.index}" id="cb">
    <redheap:showAlertBehavior message="This is message #{vs.index}"/>
  </af:commandButton>
</af:forEach>


Add Attribute to Tag Library Descriptor

The Tag Library Descriptor (TLD) describes the JSP tag itself and which attributes it supports. This is used when parsing the JSP/JSF files and also by JDeveloper for things like code completion and the property palette. Add the attribute to the tag in the TLD file:
<tag>
  <description>Describe your component here</description>
  <name>showAlertBehavior</name>
  <tag-class>com.redheap.clientbehavior.ShowAlertBehaviorTag</tag-class>
  <body-content>JSP</body-content>
  <attribute>
    <description>Message text to show in JS alert</description>
    <name>message</name>
    <required>true</required>
    <deferred-value/>
  </attribute>
</tag>

Adding Attributes to the JSP Tag Handler

Next part is the Java code. The JSP Tag Handler class is the Java implementation of the tag in the JSPX/JSF document. This is the com.redheap.clientbehavior.ShowAlertBehaviorTag class from our previous example. First thing to add is a member variable and a setter method:

    private ValueExpression message;

    /**
     * Set the message to show in the javascript alert.
     * @param message message to show in the alert
     */
    public void setMessage(ValueExpression message) {
        this.message = message;
    }

Evaluating EL Expression Attributes

In our previous sample without attributes we implemented the getBehavior method to return a snippet of javascript to instantiate a javascript object that implements the client side behavior. This time we have to pass  the message text to this client side implementation. Complicating factor is that this message text can come from an EL expression and we have to evaluate this EL expression with the correct context. Let's take the example from the introduction:
<af:forEach begin="1" end="5" varStatus="vs">
  <af:commandButton text="click to see message #{vs.index}" id="cb">
    <redheap:showAlertBehavior message="This is message #{vs.index}"/>
  </af:commandButton>
</af:forEach>

In this example we not only have to evaluate the #{vs.index} expression but we also have to do it at the correct time. We cannot do it when the tag handler is created but we have to wait until it is actually asked to create the javascript snippet as we need the correct ELContext to resolve vs.index to the actual row we are currently handling. The trick is to implement getBehaviorExpression instead of getBehavior. This can return a javax.el.ValueExpression that is evaluated at the correct time with the correct ELContext:
    @Override
    protected String getBehavior(UIComponent component) {
        return null;
    }

    @Override
    protected ValueExpression getBehaviorExpression(UIComponent component) {
        return new ShowAlertValueExpression(message);
    }

Now the burden is on com.redheap.clientbehavior.ShowAlertValueExpression which is a subclass of javax.el.ValueExpression. It has to return the value of this expression in the getValue method that gets the correct ELContext from the framework:
public class ShowAlertValueExpression extends ValueExpressionImpl {

    private ValueExpression message;

    public ShowAlertValueExpression(ValueExpression message) {
        this.message = message;
    }

    @Override
    public Object getValue(ELContext context) {
        Object msg = message.getValue(context);
        String strmsg = msg == null ? "" : msg.toString();
        return "new RedHeapShowAlertBehavior('" +
            StringEscapeUtils.escapeJavaScript(strmsg) + "')";
    }
}

It returns a snippet of javascript that instantiates a client side javascript object that handles the implementation. The actual message to show is supplied to the constructor as an argument, for example:
new RedHeapShowAlertBehavior('this is the message');

Pro Tip: we use the StringEscapeUtils from org.apache.commons.lang. This is already available on your weblogic server (albeit an older 2.1.0 version). All you have to do is to add the "WebLogic 10.3 Remote-Client" library to your project to extend your compile time classpath. Runtime is already taken care of since it is on your weblogic server.

Client Side JavaScript

Final step is to change the client side javascript object to receive and use the message attribute. We add an argument to the constructor and pass it to the initialization method which stores it in a member variable of the javascript object:
function RedHeapShowAlertBehavior(msg) {
  this.Init(msg);
}

RedHeapShowAlertBehavior.prototype.Init = function(msg) {
  RedHeapShowAlertBehavior.superclass.Init.call(this);
  this._msg = msg;
}

And of course the actual event handling code must use this value in showing the alert:
RedHeapShowAlertBehavior.prototype._handleAction =
function(event) {
  AdfAssert.assertPrototype(event, AdfUIInputEvent);
  alert(this._msg);
  event.cancel(); // cancel normal event processing
}

Usage in JSPX

Finally here is the JSPX source of a simple test page that uses the client behavior to show a message:
<af:commandButton text="click for static message" id="cb1">
  <redheap:showAlertBehavior message="static sample message"/>
</af:commandButton>
<af:commandButton text="click for dynamic message" id="cb2">
  <redheap:showAlertBehavior message="#{'dynamic sample message'}"/>
</af:commandButton>
<af:forEach begin="1" end="5" varStatus="vs">
  <af:commandButton text="click foreach message #{vs.index}" id="cb3">
    <redheap:showAlertBehavior message="message number #{vs.index}"/>
  </af:commandButton>
</af:forEach>

And here is the final result:
Custom Client Behavior triggering a javascript alert

Sample Application

As always you can download the full sample application or browse the subversion repository to look at the source code. The subversion project started as a copy of the previous sample without parameters, so look at the revision history to so the details of what was changed to support attributes. And even though it is not the most fancy application you can also see it live on Oracle Cloud.

3 comments:

  1. Super useful. It was a bit of a nightmare to implement simple placeholder text in input fields using only javascript. This is exactly the solution for which I was looking.

    ReplyDelete
  2. Hi Wilfred,

    I'd like to modify your idea in this post to perform an action when the page is loaded in the browser, but I'm having trouble finding documentation / an AdfComponentEvent for the onLoad action to replace this Javascript line:

    component.addEventListener(AdfActionEvent.ACTION_EVENT_TYPE, this._handleAction, this);

    If there is support for this, could you point me to the class I should be using here?

    My "end goal" is to create a declarative component that sets the focus on a client component as you made reference to your previous post on this subject.

    Thanks!
    Candace

    ReplyDelete
    Replies
    1. As far as I know, only the af:document supports the onLoad event. If all you want to do is set the focus to an item you can look at the initialFocusId attribute on af:document. However, a typical ADF application uses region with page fragments and you won't have the af:document in a fragment.
      Blogging about adding onLoad javascript is still on my todo-list. I have plans to create a simple JSF component that can do that. Until then you would have to resort to using ExtendedRenderKitServer#addScript from a phase listener or managed bean or simply inject a javascript snippet with af:resource. From that javascript you can find the client component and call its focus() method.

      Delete

Comments might be held for moderation. Be sure to enable the "Notify me" checkbox so you get an email when the comment is accepted and I reply.