JSF 2 Autocomplete

This is done based on this tutorial, but was modified according to some requirements:

1. The completion items should only be displayed when more than a character is typed in the input box.
2. When a completion item selected based on its label, the corresponding ID is fetched and attached to a property of a managed bean.
3. The list of completion items are dynamic and fetched from live connection from database(2nd level cache implemented).
4. Autocomplete should be accessible by using keyboard keys(tab key,up/down keys for selections, and enter key when an item selected), and mouse event definitely.
5. Autocomplete is force selection.
6. Not using Prototype for javascript functions.

Below are the snippets:
AutocompleteListener.java - the general utilities for autocomplete process.
import java.util.ArrayList;
import java.util.Map;
import javax.faces.component.UIInput;
import javax.faces.component.UISelectItems;
import javax.faces.component.UISelectOne;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;

public class AutocompleteListener {
    
 /**
  * set new item list in the list box when user type keyword in the input box.
  * @param e auto complete event when value changed.
  * @param newItems new item list populated from the database.
  * @return new item list.
  */
 public static ArrayList valueChanged(ValueChangeEvent e, ArrayList newItems) {
  UIInput input = (UIInput)e.getSource();
  UISelectOne listbox = (UISelectOne)input.findComponent("listbox");
  if (listbox != null) {
   UISelectItems items = (UISelectItems)listbox.getChildren().get(0);
   Map attrs = listbox.getAttributes();
   items.setValue(newItems);
   setListboxStyle(newItems.size(), attrs);
   listbox.setValue(null);
  }
  return newItems;
 }

 /**
  * get keyword entered by the user.
  * @return keyword entered by the user.
  */
 public static String getKeyword() {
  Map reqParams = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap();
  return reqParams.get("keyword");
 }
    
 /**
  * set the selected value from list box to the input box.
  * @param e auto complete event when value changed.
  */
 public static void completionItemSelected(ValueChangeEvent e) {
  UISelectOne listbox = (UISelectOne)e.getSource();
  if(listbox.getValue() != null) {
   UIInput input = (UIInput)listbox.findComponent("input");
   UIInput selectedValue = (UIInput)listbox.findComponent("selectedValue");

   if(input != null) {
    UISelectItems items = (UISelectItems)listbox.getChildren().get(0);
    @SuppressWarnings("unchecked")
    ArrayList selectItemsGroup = (ArrayList)items.getValue();
    for(SelectItem item : selectItemsGroup) {
     if(item.getValue().toString().equalsIgnoreCase(listbox.getValue().toString())) {
      input.setValue(item.getLabel());
      break;
     }
    }
    selectedValue.setValue(listbox.getValue());
   } else {
    selectedValue.setValue(null);
   }

   Map attrs = listbox.getAttributes();
   attrs.put("style", "display:none;");
  }
 }
    
 /**
  * set the position of the list box.
  * @param rows row of the list box.
  * @param attrs attribute object of the listbox.
  */
 private static void setListboxStyle(int rows, Map attrs) {
  if (rows > 0) {
   Map reqParams = FacesContext.getCurrentInstance()
   .getExternalContext().getRequestParameterMap();

   attrs.put("style", "display: inline; position: absolute; left: "
     + reqParams.get("x") + "px;" + " top: " + reqParams.get("y") + "px");

   // avoid only one row (selection of single row is not a change event)
   attrs.put("size", rows == 1 ? 2 : rows); 
  } else {
   attrs.put("style", "display: none;");
  }
 }
}
/*
*/

AutoCompleteBean.java - the managed bean for action and model of the autocomplete.
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Iterator;
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;
import org.apache.log4j.Logger;
import org.hibernate.Session;
import com.factory.interfaces.AutoCompleteEvent;
import com.factory.ui.utility.AutocompleteListener;
import com.factory.utility.HibernateUtil;

@ManagedBean(name="autoCompleteBean")
@ViewScoped
public class AutoCompleteBean implements Serializable, AutoCompleteEvent {

 private static Logger log = Logger.getLogger(AutoCompleteBean.class);
 private static final long serialVersionUID = 4972782275998653789L;
    
 private Integer id;
 private String name;
 private ArrayList selectionList;
 
 public ArrayList getSelectionList() {
  return selectionList;
 }
 
 public void setSelectionList(ArrayList selectionList) {
  this.selectionList = selectionList;
 }
 
 public Integer getId() {
  return id;
 }

 public void setId(Integer id) {
  this.id = id;
 }

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 /**
  * re-new autocomplete list in the list box when user type keyword in the input box.
  * @param event auto complete event when value changed.
  */
 @SuppressWarnings("rawtypes")
 @Override
 public void valueChanged(ValueChangeEvent event) {
  try {
   ArrayList completionItems = new ArrayList();
   String keyword = AutocompleteListener.getKeyword();
   if(keyword != null) {
    // this is where we get the updated values dynamically from the database and store them in the selectItem list.
    Session session = HibernateUtil.getCurrentSession();
    CompanyDAO companyDAO = new CompanyDAO();
    Company company = null;
    Iterator itr = companyDAO.findByKeyword(session, keyword).iterator(); 
    while(itr.hasNext()) {
     company = (Company) itr.next();
     completionItems.add(new SelectItem(company.getId(), company.getName()));
    }
   }
   setSelectionList(AutocompleteListener.valueChanged(event, completionItems));
  } catch(Exception e) {
   FacesContext.getCurrentInstance().addMessage(null, 
     new FacesMessage(FacesMessage.SEVERITY_ERROR, "An error has occurred.", null));
   setSelectionList(new ArrayList());
  }
 }
    
 /**
  * set the selected value from list box to the input box. 
  * @param event auto complete event when value changed.
  */
 @Override
 public void completionItemSelected(ValueChangeEvent event) { 
  AutocompleteListener.completionItemSelected(event);
 }
}
/*
*/

AutoCompleteEvent.java - an interface so that we can have different implementations for different managed beans in setting and getting the autocompletion list.
import javax.faces.event.ValueChangeEvent;

public interface AutoCompleteEvent {
 /**
  * re-new items in the list box when user type keyword in the input box.
  * @param event auto complete event when value changed.
  */
 public void valueChanged(ValueChangeEvent e);
 
 /**
  * set the selected value from list box to the input box.
  * @param event auto complete event when value changed.
  */
 public void completionItemSelected(ValueChangeEvent e);
}

testing_autocomplete.xhtml - facelet for viewing the autocomplete function. Add attribute xmlns:util="http://java.sun.com/jsf/composite/util" to the <html> element to use composite component.
...

 
 

 
    
  
 
 
 
...

autoComplete.xhtml - reusable autocomplete component. We save this file under WebContent/resources/util folder(in Eclipse).
      
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"    
    xmlns:composite="http://java.sun.com/jsf/composite">
    
     
    
                            
      
      
      
      
      
      
      
      
      
      
         
     

              
    
      
      
      
      
      
      
        
  

auto-complete.js - client script for accessing events in autocomplete. This is done using Object Literal style. We save this file under WebContent/resources/javascript folder(in Eclipse).
if ( ! com) var com = {};
if (!com.autocomplete) {
 var inputText;
 var tempInput;
 com.autocomplete = {   
   errorHandler: function(data) { 
  //alert("Error occurred during Ajax call: " + data.description) ;
 },

 updateCompletionItems: function(input, event) {
  if(inputText == document.getElementById(com.autocomplete.getId(input,':input')).value && 
    document.getElementById(com.autocomplete.getId(input,':selectedValue')).value != "") {
   return;
  }
  function getOffset(obj) {
   var curleft = 0;
   var curtop = 0;
   if (obj.offsetParent) {
    do {
     curleft += obj.offsetLeft;
     curtop += obj.offsetTop;
    } while (obj = obj.offsetParent);
   }
   return [curleft,curtop];
  }
  document.getElementById(com.autocomplete.getId(input,':autocomplete_loading')).style.visibility = 'visible';
  jsf.ajax.addOnError(com.autocomplete.errorHandler);
  var objOffset = getOffset(input);
  document.getElementById(com.autocomplete.getId(input,':selectedValue')).value = "";
  jsf.ajax.request(input, event, { 
   render:  com.autocomplete.getId(input,':listbox'),
   onevent: com.autocomplete.updateListBoxCallBack,
   keyword: document.getElementById(com.autocomplete.getId(input,':input')).value,
   x: objOffset[0],
   y: objOffset[1] + input.offsetHeight 
    });
  tempInput = input;
 },

 updateListBoxCallBack: function(e) {
  if(e.status == "success") {
   document.getElementById(com.autocomplete.getId(tempInput,':autocomplete_loading')).style.visibility = 'hidden';
   if(document.getElementById(com.autocomplete.getId(tempInput,':input')).value == "") {
    document.getElementById(com.autocomplete.getId(tempInput,':listbox')).style.display = 'none';
   }
  }
 },

 inputLostFocus: function(input) { 
  document.getElementById(com.autocomplete.getId(input,':listbox')).style.display = 'none';
  document.getElementById(com.autocomplete.getId(input,':input')).focus();
  
 },

 getId: function(input, id) {
  var clientId = new String(input.name);
  var lastIndex = clientId.lastIndexOf(':');
  return clientId.substring(0, lastIndex) + id;

 },

 submitEnter: function(obj, e) {
  var keycode;
  if (window.event) keycode = window.event.keyCode; //IE
  else if (e) keycode = e.which;  //Mozilla
  else return true;
  if (keycode == 13) {
   com.autocomplete.inputLostFocus(obj);
   return false;
  } else {
   return true;
  }
 },
 
 resetInputValue: function(input) {
  if(document.getElementById(com.autocomplete.getId(input,':selectedValue')).value == "") {
   inputText = document.getElementById(com.autocomplete.getId(input,':input')).value;
   document.getElementById(com.autocomplete.getId(input,':input')).value = "";
  }
 },

 getInputValue: function(input) {
  if(document.getElementById(com.autocomplete.getId(input,':selectedValue')).value != "") {
   inputText = document.getElementById(com.autocomplete.getId(input,':input')).value;
  }
  document.getElementById(com.autocomplete.getId(input,':input')).value = "undefined" == typeof(inputText) ? 
    document.getElementById(com.autocomplete.getId(input,':input')).value : inputText;
 }
 };
}

auto-complete.css : decoration for the list box. We save this file under WebContent/resources/css folder(in Eclipse).
.auto-complete-selectbox {
   border: 1px solid #BBBBBB;
   padding: 1px;
   background-color: #F8F8F8;
   overflow: auto;
   height: 80px;
}

.auto-complete-selectbox select{
   border:none;
}


the auto-complete.js and the auto-complete.css can be put inside the composite component autoComplete.xhtml to make it a truly independent component, but for performance wise, they are put in the testing_autcomplete.xhtml so that css will be loaded at the top of the page and javascript at the bottom of the page.

3 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hi! I have a JSF project with netbeans and I can't make work this solution. Any other advices you can give me?

    ReplyDelete
  3. Hi Ana,

    sorry for late reply, had been busy to check this blog. Anyway, it'd be better to use jsf plugins such as primeFaces or RichFaces etc to achieve this goal because it's a lot easier and works better. In my case, I just worked on a lightweight R n D project that use jsf2.0 + jquery with no other dependencies.
    But if you want to do this yourself, I think you need to refer to http://www.ibm.com/developerworks/java/library/j-jsf2fu-0410/index.html

    ReplyDelete