Sunday, October 31, 2010

load javascript asynchronously using managedXHR

A nice explanation by Steve Souders at youtube.com/watch?v=52gL93S3usU mainly on loading JavaScript asynchronously.

We can try out each technique by referring to this page.

One of the common issue is the undefined error due to dependency between JavaScript files when scripts loaded asynchronously. For example when using jquery plugins, the core jquery file, followed by the plugins files and lastly the inline scripts for function initialization must be loaded in consecutive manner. One of the technique to solve this issue is by using managed XHR(assume all files are from the same domain).

Below are the snippets:

1.Put this snippet at the bottom of the page as inline code.
if ( "undefined" == typeof(ManagedXHR) || !ManagedXHR ) {
 var ManagedXHR = {};
}

ManagedXHR.Script = {
 queuedScripts: [],

 loadScriptXhrInjection: function(url, onload, bOrder) {
  var iQueue = ManagedXHR.Script.queuedScripts.length;
  if ( bOrder ) {
   var qScript = { response: null, onload: onload, done: false };
   ManagedXHR.Script.queuedScripts[iQueue] = qScript;
  }

  var xhrObj = ManagedXHR.Script.getXHRObject();
  xhrObj.onreadystatechange = function() { 
   if ( xhrObj.readyState == 4 ) {
    if ( bOrder ) {
     ManagedXHR.Script.queuedScripts[iQueue].response = xhrObj.responseText;
     ManagedXHR.Script.injectScripts();
    }
    else {
     var se = document.createElement('script');
     document.getElementsByTagName('head')[0].appendChild(se);
     se.text = xhrObj.responseText;
     if ( onload ) {
      onload();
     }
    }
   }
  };
  xhrObj.open('GET', url, true);
  xhrObj.send('');
 },

 injectScripts: function() {
  var len = ManagedXHR.Script.queuedScripts.length;
  for ( var i = 0; i < len; i++ ) {
   var qScript = ManagedXHR.Script.queuedScripts[i];
   if ( ! qScript.done ) {
    if ( ! qScript.response ) {
     // STOP! need to wait for this response
     break;
    }
    else {
     var se = document.createElement('script');
     document.getElementsByTagName('head')[0].appendChild(se);
     se.text = qScript.response;
     if ( qScript.onload ) {
      qScript.onload();
     }
     qScript.done = true;
    }
   }
  }
 },

 getXHRObject: function() {
  var xhrObj = false;
  try {
   xhrObj = new XMLHttpRequest();
  }
  catch(e){
   var aTypes = ["Msxml2.XMLHTTP.6.0", 
        "Msxml2.XMLHTTP.3.0", 
        "Msxml2.XMLHTTP", 
        "Microsoft.XMLHTTP"];
   var len = aTypes.length;
   for ( var i=0; i < len; i++ ) {
    try {
     xhrObj = new ActiveXObject(aTypes[i]);
    }
    catch(e) {
     continue;
    }
    break;
   }
  }
  finally {
   return xhrObj;
  }
 }
};

2.Call our external JavaScript files like this:


Inside the init.js, we can call the initialization codes. By using this technique all external files can be loaded asynchronously and at the same time it preserves the order the code execution.

We can use httpWatch to see the difference.

Saturday, October 30, 2010

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.

Sunday, October 17, 2010

JSF 2 Pass enum value as parameter

Taken from http://stackoverflow.com/questions/3916871/passing-a-enum-value-as-a-parameter-from-jsf .
In short :
//this is your enum
public enum Type {
    PROFILE_COMMENT,
    GROUP_COMMENT
} 
//this is your managed bean
@ManagedBean
@SessionScoped
public class myBean {
    private Type type;
    // getter and setter
    public void Test(Type t){
        System.out.println(t);
    }
}
In facelet, we pass the enum as string:


add this to the faces-config.xml so that enum conversion can be handled by jsf

   java.lang.Enum
   javax.faces.convert.EnumConverter

Saturday, October 16, 2010

Get selected row in h:dataTable via DataModel

This is taken from Balusc answer at http://stackoverflow.com/questions/3951263/jsf-command-button-inside-a-jsf-data-table .
In short, in the managed bean:
@ManagedBean
@ViewScoped
public class UserManager {
    private List doctors;
    private DataModel doctorModel;

    @PostConstruct
    public void init() {
        doctors = getItSomehow();
        datamodel = new ListDataModel(doctors);
    }

    public void getSpecificDoctor() {
        Doctor selectedDoctor = doctorModel.getRowData();
        //do the logic here
    }
}
/*
*/

In the facelet:

   

JSF 2 : How to pass parameter

The answers in this link at
http://stackoverflow.com/questions/3599948/jsf2-action-parameter describes techniques to pass parameter in JSF 2.

In Short, the common ways are:
1. via action method:

So in the managed bean our method would be like this:
public String action(String parameter) {}

2.via f:param :

   

In the managed bean, we can get the parameter value like this:
@ManagedProperty("#{param.foo}")
private String foo;
or this:
String foo = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("foo");

Sunday, October 10, 2010

Tuesday, October 5, 2010

Issue with JSF and Jquery Id

When we use JSF ,by default any elements inside h:form will be appended with the form id like myform:myid when it's rendered as plain html.
jquery has trouble to identify this id with ':' as for example $("#myform:myid").html("this doesn't work.") will not work. Hence we can either

1. escape using "\\"
$("#myform\\:myid").html("this works");
or
2. add prependId=false in the h:form.
   
      ...
      
   

Now we can use jquery function like normal:
$("#myid").html("this works.");

or
3. if we do not want to add prependId to the h:form, we can use other jquery selectors such as class selector like :
$(".myclass").html("this will work as well."); .

Rerender specific areas using ajax f:ajax in ui:repeat

below is an example how to rerender specific areas using ajax f:ajax in ui:repeat:

  

In this example we just want to rerender specific elements.So,

render=":myform:myid :myform:myaddress" - these are the areas where we want to rerender after callback. Two areas will be re-rendered are elements with id=myid and id=myaddress , using a space as separator.

We need to append parent id :myform in the render attribute of f:ajax, else f:ajax will give warning render area cannot be found.