8 May 2011

InputText with suggestion on demand

Introduction 
ADF Faces provide us by quite handy tag af:autoSuggestBehavior. It could be used together with some input control in order to implement very common use-case when  a user typing some text is suggested by a drop-down list with some values to be selected and displayed in the input field. The following screenshot shows an example of such behavior:

The jspx code for this example is very simple:

        <af:inputText label="Currency" id="i21">
          <af:autoSuggestBehavior 
              suggestedItems="#{InputSuggestBean.onCurrencySuggest}"/>
        </af:inputText>

The af:autoSuggestBehavior tag needs to be bounded to a backing bean method returning a list of items to be suggested. The method has only one String argument containing submitted user's value. Depending on this value you can implement your own logic for the returning list like filtering, sorting, etc. For example, the following backing bean method returns currency codes that match user's input string:

    private final static String[] ccys = {"USD", "EUR", "UAH", "CAD"};

    public List onCurrencySuggest(String inputValue) {
        ArrayList<SelectItem> suggestItems = new ArrayList<SelectItem>();
        for (String s: ccys) {
            if (s.startsWith(inputValue)) 
                suggestItems.add(new SelectItem(s));
        }
        return suggestItems;
    }

Of course, you can implement your backing bean method getting suggested items from ADF BC layer in order to retrieve them from database or from other data sources. You can find detailed example of this feature in the ADF Code Corner Article posted by Frank Nimphius.

Suggestion on demand
But there is another common use-case when users don't need any auto-suggestion for their input, and they want to be suggested on demand only. For example, by pressing "Ctrl-H", a user is provided by previously submitted values for this input field. Something like a history of values.
In this post I'm going to show two different implementations of this use-case. The first one is based on modified af:autoSuggestBehavior tag and the second implementation is built using usual af:popup tag.

Modified af:autoSuggestBehavior
  The jspx definition of inputText looks as usual:

        <af:inputText label="Modified input" id="it1">        
          <af:autoSuggestBehavior 
            suggestedItems="#{InputSuggestBean.onSuggest}"/>
        </af:inputText>

And the backing bean method looks like this:

    public List onSuggest(String string) {
        ArrayList<SelectItem> selectItems = new ArrayList<SelectItem>();
      
        //Let's assume these are history values:
        selectItems.add(new SelectItem("One"));
        selectItems.add(new SelectItem("Two"));        

        return selectItems;
    }

af:autoSuggestBehavior tag  renders some JavaScript object AdfAutoSuggestBehavior responsible for auto-suggestion functionality. It adds a number of event listeners to the inputText for different event types like 
onKeyUp, onBlur, onFocus, etc.

In order to prevent default behavior of af:autoSuggestBehavior tag I have to override its onKeyUp listener and do some JavaScript coding:

<af:document id="d1">      
 <af:resource type="javascript">
  var initialized = false; //Initialization flag
  
  initMySuggestBehavior = function () {
      if (!initialized) { 
          
         //Saving default _fireKeyUp function
         AdfAutoSuggestBehavior.prototype._fireKeyUpEx = 
            AdfAutoSuggestBehavior.prototype._fireKeyUp;

         //Writing proxy for _fireKeyUp function
         AdfAutoSuggestBehavior.prototype._fireKeyUp = function (e) {
           //Getting the event's source and its content                
           var input = e.getSource();
           if (input instanceof AdfUIEditableValue) 
             var inputContent = AdfDhtmlEditableValuePeer.GetContentNode(input);
             //If user has pressed ctrl H then fire suggestion
           if ("ctrl H" == e.getKeyStroke().toMarshalledString()) {
              AdfAutoSuggestBehavior.prototype._autoSuggest(input);
              //Just for better visualization
              if (inputContent.value.length == 0) inputContent.value = "...";
              }

           //This will prevent default suggestion firing for every input character
           if (inputContent) this._nodeLength = inputContent.value.length;
          
           //Calling default event listener we saved before
           AdfAutoSuggestBehavior.prototype._fireKeyUpEx(e);
        };
             
        //Looking for our inputText
        var inp = AdfPage.PAGE.findComponentByAbsoluteId("it1");
         
        /* At this moment AdfAutoSuggestBehavior has already initialized
         * and added his default listeners to the inputText.
         * We need to clear them and reinitialize with our overridden 
         * _fireKeyUp function.
         */  
        inp.setProperty("clientListeners", null);
        AdfAutoSuggestBehavior.prototype.initialize(inp);
         
        /* Adding a simple listener for onKeyDown event in order to 
         * prevent default browser behavior for Ctrl-H            
         */
        inp.addEventListener(AdfUIInputEvent.KEY_DOWN_EVENT_TYPE, myKeyDown, this);
                
        //We've done it.
        initialized = true;
     }
  }
//Preventing default browser behavior for Ctrl-H
 myKeyDown = function (event) {
    if ("ctrl H" == event.getKeyStroke().toMarshalledString())
       event.cancel();
  }
</af:resource>
...      

I'm going to call initMySuggestBehavior JavaScript  function in a phase listener written for the f:view tag:
<f:view beforePhase="#{InputSuggestBean.viewPhaseListener}">
    public void viewPhaseListener(PhaseEvent phaseEvent) {
        if (phaseEvent.getPhaseId() == PhaseId.RENDER_RESPONSE) {
          FacesContext fctx = FacesContext.getCurrentInstance();
          ExtendedRenderKitService ks = 
              Service.getRenderKitService(fctx, ExtendedRenderKitService.class);
          StringBuffer script = new StringBuffer();
          script.append("window.initMySuggestBehavior()");
          ks.addScript(fctx, script.toString());            
        }

    }

The result of our modifications looks like this:


Using af:popup

The jspx page for this implementation looks like this:

    <af:popup id="ctrlHPopup" contentDelivery="lazyUncached"
              animate="false">
      <af:selectOneListbox 
               id="sol4"
               simple="true"
               autoSubmit="false"
               >
        <f:selectItems value="#{InputSuggestBean.popupSuggestion}" id="si7"/>
        <af:clientListener type="keyDown" method="enterSelection"/>
        <af:clientListener type="click" method="clickSelection"/>
      </af:selectOneListbox>
    </af:popup>

    <af:panelLabelAndMessage label="Input with popup" id="plam1">
      <af:inputText id="it2" simple="true">
       <af:clientListener method="ctrlHKeyDown" type="keyDown"/>
      </af:inputText>
    </af:panelLabelAndMessage>

I have a popup and selectOneListbox component inside it. The listbox get suggested items from the backing bean property popupSuggestion and it has client listeners to submit selected values when Enter is pressed or mouse is clicked. The inputText has a listener to fire popup with suggested items when Ctrl-H is pressed.
The backing bean has the following method:

    public List getPopupSuggestion() {
        ArrayList<SelectItem> selectItems = new ArrayList<SelectItem>();
      
        //Let's assume these are history values
        selectItems.add(new SelectItem("The first value"));
        selectItems.add(new SelectItem("The second value"));        

        return selectItems;
    }

And for sure we have some JavaScript implementation of our listeners:
        //Firing popup and preventing default browser behavior for Ctrl-H
        ctrlHKeyDown = function (event) {
            if ("ctrl H" == event.getKeyStroke().toMarshalledString()) {
              showPopup();     
              event.cancel();              
            }   
        }
        
        //Firing popup
        showPopup = function(){
         //Looking for popup component
         var popup = AdfPage.PAGE.findComponentByAbsoluteId("ctrlHPopup");
         //Define popup alignment 
         var hints = {}; 
         hints[AdfRichPopup.HINT_ALIGN_ID] = 
           AdfPage.PAGE.findComponentByAbsoluteId("it2").getClientId();
         hints[AdfRichPopup.HINT_ALIGN] = AdfRichPopup.ALIGN_AFTER_START;
         //Showing
         popup.show(hints);
        }

        //Hiding popup
        hidePopup = function(){
         var popup = AdfPage.PAGE.findComponentByAbsoluteId("ctrlHPopup");
         popup.hide();
        }    
        
        //If Enter is pressed 
        enterSelection = function(event){         
         if (AdfKeyStroke.ENTER_KEY == event.getKeyStroke().getKeyCode()) {
           setSelectedValue(event);  
         }
        }
        
        //If mouse is clicked
        clickSelection = function(event){         
           setSelectedValue(event);  
        }

        //Submit selected value and hide the popup
        setSelectedValue = function(event) {
            var items = event.getSource();
            var selectedItem = items.getSelectItems()[items.getValue()];
            var inputText = AdfPage.PAGE.findComponentByAbsoluteId("it2");
            var inputContent = AdfDhtmlEditableValuePeer.GetContentNode(inputText);
            inputContent.value = selectedItem.getLabel(); 
            hidePopup();    
        }

And the result of our work looks like this:


That's all. You can download sample application for this post.

No comments:

Post a Comment

Post Comment