Wednesday May 16, 2012

Placeholder Watermarks with ADF 11.1.2

I'm sure you're all familiar with the concept of watermarking an input field to provide the user with a greyed out hint as to what to do with it.  This is often used in search boxes or UIs where space is at a premium and you really don't have room for a label. Here's a small screenshot that shows what I mean:

Image of watermark in use

As you can see,  both the filter field and multi-line field have such text.  As soon as the user starts to enter values in these fields the watermark will disappear only to reappear if the user clears out the field.  In the past, there have been various JavaScript based solutions to this, but the HTML5 spec introduces a common way of doing this using an attribute on the <input> element called placeholder. Alas, only Chrome and FireFox have implemented this in their later versions, although it's on the list for IE 10. 

Now I probably won't be giving too much away if I let slip that placeholder support might possibly be standard in a future version of ADF Faces, but for now, I'm working in 11.1.2.2, so here's a simple implementation in the form of a custom JSF clientBehavior that will do it. 

For this, I actually took inspiration from a recent blog posting from Donatas Valys: Set Initial Focus component for Oracle ADF Faces.  He hit upon the smart idea of using a client behavior to mark a particular component as having initial focus, I've used essentially the same technique here, although extended a little bit because of the nature of the job I'm doing.

Create your Tag Library

So the first step will to create the tag (it will be called <afg:setWatermarkBehavior>) in your project.  Just select New > Web Tier > JSF/Facelets > Facelets Tag Library.  On the first page of the creation wizard, choose Deployable or Project Based, the choice is yours. For convenience I chose Project Based, then on Step 2 provide a file name and a namespace. I used adfglitz.taglib.xml and http://xmlns.oracle.com/adf/faces/glitz respectively, choose suitable values for your implementation. This step will do everything to register the Tag Library with the project (you'll see an entry is added to web.xml) 

Define the Behavior Definition 

Now you can edit the tag file and add the new behavior (or as I would have it "behaviour"). The taglib editor provides an overview view to make this simple:

Tag Lib editor

The important things to note here are:

  • The namespace (http://xmlns.oracle.com/adf/faces/glitz), we'll need that when adding this tag to the page. 
  • The name of the behavior tag - setWatermarkBehavior
  • The ID of the behavior tag - oracle.demo.adfglitz.watermark - I'll use this to associate an implementation class with the tag
  • The attributes.  Note that I've defined one attribute for the tag called value. I'll use this to pass the actual text that needs to be displayed in the placeholder.

You can switch to the source view of the editor and provide more information such as a friendly description of the taglib and tag, but you don't actually need to. 

Implement the Behavior

 Next we need to actually create a class that provides the implementation of the behavior. This needs to extend javax.faces.component.behavior.ClientBehaviorBase and implement javax.faces.component.StateHolder. The latter interface implementation is to ensure that the String passed in as the value of the tag is persisted and will survive re-builds of the component tree. 

The basic class definition therefore looks like this: 

@FacesBehavior("oracle.demo.adfglitz.watermark")
public class SetWatermarkBehavior extends ClientBehaviorBase implements StateHolder {...}

Note that I use the FacesBehavior annotation  to do the wiring between this implementation class and the taglib using the ID attribute defined above.

The other thing we need to implement the tag contract is the "value" that must be passed in as a  required attribute for the tag.  This is done with a simple member variable (String value;) and an associated public getter and setter pair on the class - getValue(), setValue() - standard stuff. We also have to manage the storage of this value by implementing the StateHolder interface. The code for that is not very interesting so I'll not reproduce it here, you can see it in the complete java class though.

The core method within the behavoir class is the getScript() method. This is called to encode the JavaScript  to send down with the enclosing component, however, we're going to subvert it a little - let me explain why.  If you have a clientBehavior associated with an inputItem then any script that you return from the getScript() method will be associated with a value change listener on the component.  In this case, that's not what we want, rather than applying the watermark when the value of the input changes, we want to apply it at the point in time the component is rendered. So to achieve this we actually just use and abuse the getScript() call and use the Trinidad ExtendedRenderKitService to queue up execute the JavaScript we need in a just in timely way.

Here's the implementation of the method:

//Constants used by getScript() defined at class level
private static final String SCRIPT_PREFIX = "addWatermarkBehavior(\"";
private static final String SCRIPT_ARG_SEPARATOR = "\",\""; 
private static final String SCRIPT_SUFFIX = "\");"; 
 
@Override
public String getScript(ClientBehaviorContext ctx) {
  UIComponent component = ctx.getComponent();
  //only contiune if the component is currently rendered
  if (component.isRendered()) {
    String componentType = deduceDOMElementType(component);
    //Only continue if the component is a valid type
    if (!componentType.equals(UNSUPPORTED_ELEMENT)) {
      String wmText = (getValue() == null) ? EMPTY_DEFAULT : getValue();
      StringBuilder script = new StringBuilder(SCRIPT_PREFIX);
      script.append(ctx.getSourceId());
      script.append(SCRIPT_ARG_SEPARATOR);
      script.append(componentType);
      script.append(SCRIPT_ARG_SEPARATOR);
      script.append(wmText);
      script.append(SCRIPT_SUFFIX);

      //We don't have an init event, just valueChange so push the code this way
      FacesContext fctx = ctx.getFacesContext();
      ExtendedRenderKitService extendedRenderKitService =
               Service.getRenderKitService(fctx, ExtendedRenderKitService.class);
      extendedRenderKitService.addScript(fctx, script.toString());
    }
  }
  //And return nothing as we don't need a valuechangeListener
  return "";
}

Things to note here.  We could of course encode the entire JavaScript  function within the script string generated above. However, given that in my case, I have several uses of the behavior in the app it makes sense to shove the detail of that code into a common .js file. I already have this available on the page and call a simple function - addWatermarkBehavior(), passing the relevant component ID, type and placeholder value.  That JavaScript can be seen below.

Another point is that getScript() uses the convenience method deduceDOMElementType() which, from the component and its attributes works out: First of all if it's a valid component on which to do anything, and secondly if the placeholder will need to be set on an html <input> element or an <textarea>.

//Constants used by deduceDOMElementType() defined at class level
private static final String UNSUPPORTED_ELEMENT = "unsupported";
private static final String TEXTAREA_ELEMENT = "textarea";
private static final String INPUT_ELEMENT = "input";

private String deduceDOMElementType(UIComponent component) {
  String componentType = UNSUPPORTED_ELEMENT;
  //work out the correct component type
  if (component instanceof RichInputText) {
    //In this case we may have a multi-line item but assume intially that this is not the case
    componentType = INPUT_ELEMENT;
    //Now check for the rows attribute to see if this is multi-line
    Map<String, Object> compAttr = component.getAttributes();
    if (compAttr.containsKey("rows")) {
      int rows = (Integer)compAttr.get("rows");
      if (rows > 1) {
         componentType = TEXTAREA_ELEMENT;
      }
    }
  } else if (component instanceof RichInputDate || 
             component instanceof RichInputListOfValues ||
             component instanceof RichInputComboboxListOfValues) {
    //These all resolve to inputs at the DOM level
    componentType = INPUT_ELEMENT;
  }
  return componentType;
}

The JavaScript

 As I mentioned above, rather than stream down reams of script for each component I have a standard JavaScript file attached to my page using the <af:resource> tag and in that I've implemented the simple function to locate the correct component on the page and apply the correct placeholder text. You could also use this method as a place to add a script based solution to browsers that don't support placeholder. Here's the  function:

function addWatermarkBehavior(componentId, type, text){
    var sourceInput = AdfPage.PAGE.findComponent(componentId);
    var domElemement = AdfAgent.AGENT.getElementById(sourceInput.getClientId());
    var targetArray = domElemement.getElementsByTagName(type);
    targetArray[0].placeholder = text;
}

As you can see, pretty short and sweet, you could of course add various validations to check you have a real element  etc. but let's keep it simple.

Using the Behavior

So that's about it. The final point is, of course how to use this. Well all you need to do is register the namespace in your page or fragment as I've done here using the afg: prefix:

<ui:composition xmlns:ui="http://java.sun.com/jsf/facelets" 
                xmlns:af="http://xmlns.oracle.com/adf/faces/rich"
                xmlns:afg="http://xmlns.oracle.com/adf/faces/glitz"
                xmlns:f="http://java.sun.com/jsf/core"> 

And then use the tag thus:

<af:inputText ...>
  <afg:setWatermarkBehaviour value="Filter Items"/> 
</af:inputText> 

 Enjoy.

About

Hawaii, Yes! Duncan has been around Oracle technology way too long but occasionally has interesting things to say. He works in the Development Tools Division at Oracle, but you guessed that right? In his spare time he contributes to the Hudson CI Server Project at Eclipse
Follow DuncanMills on Twitter

Note that comments on this blog are moderated so (1) There may be a delay before it gets published (2) I reserve the right to ignore silly questions and comment spam is not tolerated - it gets deleted so don't even bother, we all have better things to do with our lives.
However, don't be put off, I want to hear what you have to say!

Search

Archives
« July 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
   
       
Today