Tuesday 3 July 2012

Creating custom client behavior

ADF's client behaviors is a wonderful feature. It allows to add custom javascript code to any component it is attached to (refer to list of default ADF behaviors if you don't know what they can do). Unfortunately, they are not in public API, but you can use ADF's internal classes (at your own risk...).
Below I will describe how to create your own client behavior in 11.1.1 release. I will focus on how to create appropriate classes and files and not the actual javascript code been added.

1. Create a tag handler class, that extends oracle.adfinternal.view.faces.taglib.behaviors.BehaviorTag:
package components.behaviors;
import javax.faces.component.UIComponent;
import oracle.adfinternal.view.faces.taglib.behaviors.BehaviorTag;

public class CustomBehavior extends BehaviorTag {
    public CustomBehavior() {
        super();
    }
    protected String getBehavior(UIComponent uIComponent) {
        return null;
    }
}
Method getBehavior must return a piece of javascript code that calls client constructor (we will implement it later).

2. If you want your behavior to accept some attributes, add fields of type ValueExpression and setters for them:
package components.behaviors;
import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import oracle.adfinternal.view.faces.taglib.behaviors.BehaviorTag;

public class CustomBehavior extends BehaviorTag {
  
    private ValueExpression value;
  
    public CustomBehavior() {
        super();
    }

    protected String getBehavior(UIComponent uIComponent) {
        return null;
    }

    public void setValue(ValueExpression value) {
        this.value = value;
    }
}
3. Now create a JSF tag in yout taglib, that points to our tag handler class. Add all needed attributes:
<tag>
  <name>customBehavior</name>
  <tag-class>components.behaviors.CustomBehavior</tag-class>
  <body-content>JSP</body-content>
  <attribute>
    <name>value</name>
    <deferred-value/>
  </attribute>
</tag>
4. Now we should create a javascript object, that will define our custom behavior.
Create a *.js file (in the same package "components.behaviors" or any other). You can name it anything you want, but it's better to name it the same as containing object. It is also suggested, that javascript object name has a prefix denoting your application (because these names are global in javascript). For example, all ADF objects' names start with "Adf": AdfDomUtils, AdfUIComponent, etc.
Let's name our object CaCustomBehavior (I picked prefix 'Ca'), create file src/component/behaviors/CaCustomBehavior.js. For now let's leave it empty and create an ADF js-feature.

5. Open META-INF/adf-js-features.xml file in the project that holds your taglib. (create one if it does not exist). Add a "feature" tag to the "features" element. Like this:
<?xml version="1.0" encoding="windows-1251" ?>
<features xmlns="http://xmlns.oracle.com/adf/faces/feature">
  <feature>
    <feature-name>CaCustomBehavior</feature-name>
    <feature-class>components/behaviors/CaCustomBehavior.js</feature-class>
  </feature>
</features>
feature-name preferably must correspond to your javascript object's constructor (that we are yet to create).

6. Let's tell ADF to load the js-feature we created, when our behavior is being rendered. To do this we must override method getFeatureDependency() in tag-handler class CustomBehavior to return the name of the js-feature we just created:
    @Override
    protected String getFeatureDependency() {
        return "CaCustomBehavior";
    }

7. Let's add some basic code in CaCustomBehavior.js:
CaCustomBehavior.InitClass = function(){
    // initialize class (e.g. create constants)
}

// create subclass
AdfObject.createSubclass(CaCustomBehavior, AdfClientBehavior);

// constructor
function CaCustomBehavior(value) {
    this.Init(value);
}

// object initialization
CaCustomBehavior.prototype.Init = function(value){
    AcHighlightBehavior.superclass.Init.call(this);
    this._value = value;
}

// initialize with component
CaCustomBehavior.prototype.initialize = function(component){
    // TODO: implement behavior logic here
}
Notice that our constructor accepts 1 argument - "value". But, essentially, your behavior's constructor can have any number of arguments. When there is too many of them, you could pass one object with a multiple fields, holding your arguments.
I will not describe every line of upper code here, main function you should notice is initialize(component). This function is called every time a component appears on the page. This is, actually, the place to add your custom behavior. For example, you can register event listeners on a component. You can set it to something like alert(this._value) to test that your behavior is loaded. I suggest to do it before actually writing client-side code.

8. And, finally, the last part. Define in your tag-handler how to create a client-side behavior object by implement getBehavior method. It must return a call to javascript object's constructor with "new", and without a semicolon(or you will get a javascript exception). Something like this: new CaCustomBehavior('arg').
In our getBehavior we will also calculate ValueExpression value:
    protected String getBehavior(UIComponent uIComponent) {
        ELContext elContext = FacesContext.getCurrentInstance().getELContext();
        String val = value == null ? "" : ComponentUtils.resolveString(value.getValue(elContext));
        return "new CuCustomBehavior('" + val + "')";
    }

And that's it. You can do a lot of things with a client behaviors, once you get accustomed with them, there are really useful, and also provide easy re-usability. (instead of javascript resources and clientListeners). Some things I've done with behaviors: hints inside inputText, custom autosuggest (maybe I will write post with this examples, especially if someone asks =) ) and many more...
Please leave your comments or questions. Say if this was useful to you.

7 comments:

  1. Thanks so much! This blog really has become some kind of table-book for me. =)

    ReplyDelete
  2. hi i want to add watermark effect for input text box in Jdev 11.1.1.6. so i tried to add it but dont know extacly where should I set placholder attribute??

    Can you please give me some idea for the same.

    ReplyDelete
  3. Hi.
    You can find an \ element, associated with a component by adding "::content" to the component's id and using AdfAgent.AGENT.getElementById function to find dom element by id.
    If you are making a client behavior, as described in the post, you can use this code in javascript:
    BehaviorClass.prototype.initialize = function(component){
    var el = AdfAgent.AGENT.getElementById(component.getClientId() + "::content");
    if (el && el.placeholder) el.placeholder = "...";
    }

    By the way, 'placeholder' is not supported in IE9 and IE8...

    ReplyDelete
  4. Alternatively, you can use AdfDhtmlEditableValuePeer.GetContentNode function to find INPUT node for inputText component.

    ReplyDelete
  5. Hi Daniel,
    Nice post...
    But can you write another post showing the use of custom client behavoior..??
    I am just missing what can be done with custom behaviour which cannot be done javascript resources and clientListeners...

    ReplyDelete
  6. Hi Daniel, I really like your post.

    Here's what I am trying to do.

    Modify PopupBehavior and add some sliding effects using jquery.
    How can I replace default behavior with my custom one ?

    Thanks

    Mikhail

    ReplyDelete
    Replies
    1. Hi Mikhail. Sorry for long wait for an answer.
      I stopped working with ADF Faces recently, so it's kinda hard to give a detailed answer...
      As far as I remember it's really hard to redefine ADF popup's. Actually you need to override not the ShowPopupBehavior (as it's only calling Popup.show()), but the Popup itself. I see two ways to achieve this: quick-and-dirty: override ADF javascript methods in your *.js files (methods like AdfDhtmlNoteWindowPopupSelector.prototype.show), and hard-and-clean (define new type of popup, extending existing, for example, NoteWindow). Though I am not sure the latter is even possible... Look through ADF's javascript codes and try to find which methods you need to override.

      Delete