Build List of Values in JBoss-Seam using RichFaces suggestionbox

By | July 8, 2009

List of values, also known as LOV, is a UI presentation pattern for many-to-one associations between entities. The lookup field contains typically the unique key of the related entity. The additional information of related entity is displayed beside the lookup field for referential purposes.
The data entry in the lookup field is supported by the list of allowed values, which is displayed in some kind of pop-up allowing one value to be picked-up.
This functionality was widely used in legacy environments giving the novice user a guideline and not forcing the power user to use the selection for the well known values.
In the pre AJAX times the most common solution was the pop-list loaded with all possible values, hated by power user and not effective for the large amount of values.
Another approach, i.e. generated by JBoss seam-gen utility reuses the search and filtering form in order to select the required object. This is a valuable solution for the huge amount of data without clear visible unique key, but not always accepted because of the interruption of current workflow.

This tutorial shows how to build the list of values in JBoss-Seam with following features:

  • adjust the list of possible values immediately as the user types in lookup field
  • displays the link to the referenced object
  • icon indication of LOV availability
  • display all possible values on demand, before starting typing in lookup field

that looks like this:

seam_lov1

After selection the value and posting the form the LOV lookup field will looks like this:

seam_lov2

Let assume we have to entities: customer and project . The project entity have the many-to-one association to the customer. Like this:

@Entity
public class Customer implements Serializable {
	private Long id;
	private String code;
	private String name;
	private String city;
	private String zip;
	private String street;
	private Country country;
	private boolean active;

	@Id @GeneratedValue
	public Long getId() {
		return id;
	}

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

	@Length(min=3, max=5) 
	@NotNull
	@Column(unique=true)
	public String getCode() {
		return code;
	}

	public void setCode(String code) {
		this.code = code;
	}

	@Length(max=50) @NotNull
	public String getName() {
		return name;
	}

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

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getZip() {
		return zip;
	}

	public void setZip(String zip) {
		this.zip = zip;
	}

	public String getStreet() {
		return street;
	}

	public void setStreet(String street) {
		this.street = street;
	}

	@Enumerated(EnumType.STRING)
    @Column(length=2)
	public Country getCountry() {
		return country;
	}

	public void setCountry(Country country) {
		this.country = country;
	}

	public boolean isActive() {
		return active;
	}

	public void setActive(boolean active) {
		this.active = active;
	}

}

and

@Entity
public class Project implements Serializable{
	private Long id;
	private String code;
	private String description;
	private boolean active;
	private Customer customer = new Customer();
	
	private List<Task> tasks = new ArrayList<Task>();

	@Id @GeneratedValue
	public Long getId() {
		return id;
	}

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

	@NotNull
	@Length(min=3, max=5)
	public String getCode() {
		return code;
	}

	public void setCode(String code) {
		this.code = code;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public boolean isActive() {
		return active;
	}

	public void setActive(boolean active) {
		this.active = active;
	}

	@ManyToOne
	@NotNull
	public Customer getCustomer() {
		return customer;
	}

	public void setCustomer(Customer customer) {
		this.customer = customer;
	}

	@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "project")
	public List<Task> getTasks() {
		return tasks;
	}

	public void setTasks(List<Task> tasks) {
		this.tasks = tasks;
	}

}

The customer entity have the visible unique key – the code.
I assume we have already the the CustomerHome und CustomerList classes generated by the seam-gen utility.
We need to provide the unique key finder in in the CustomerHome like this:

	@Transactional
	public Customer findByUK(String code) {
		Customer customer = null;
		try {
			customer = (Customer) getEntityManager().createQuery(
					"select c from Customer c where c.code = ?1")
					.setParameter(1, code).getSingleResult();
		} catch (NoResultException e) {
			return null;
		}
		return customer;
	}		

Now we extend the UI. In ProjectEdit.xhtml we add a simple lookup field:

<s:decorate id="customerField" template="layout/edit.xhtml">
	<ui:define name="label">Customer</ui:define>
	<h:inputText id="customer" 
			style="text-transform:uppercase" required="true" 
			value="#{projectHome.instance.customer}" />
</s:decorate>

Since the lookup filed is the plain text field and we reference directly the customer entity by value property, we need the custom entity converter here which converts the customer entity to and from the string inside the lookup field:

package net.dobosz.restseam.converter;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

import net.dobosz.restseam.action.CustomerHome;
import net.dobosz.restseam.model.Customer;

import org.jboss.seam.Component;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.intercept.BypassInterceptors;

@Name("customerConverter")
@BypassInterceptors
@org.jboss.seam.annotations.faces.Converter(forClass=Customer.class)
public class CustomerConverter implements Converter {

	private CustomerHome customerHome = (CustomerHome) Component.getInstance(CustomerHome.class);

	@Override
	public Object getAsObject(FacesContext context, UIComponent comp, String value) {
		if(value == null || value.trim().length() == 0)
	          return null;
		Customer customer = null;
        customer = customerHome.findByUK(value.toUpperCase());
		return customer;
	}

	@Override
	public String getAsString(FacesContext context, UIComponent comp, Object value) {
		return ((Customer)value).getCode();
	}

}

then we integrate the converter seam component in the lookup field:

<s:decorate id="customerField" template="layout/edit.xhtml">
	<ui:define name="label">Customer</ui:define>
	<h:inputText id="customer" 
               converter="#{customerConverter}"
			style="text-transform:uppercase" required="true" 
			value="#{projectHome.instance.customer}" />
</s:decorate>

Before we start building suggestion box in the lookup field, we need some kind of finder which give us a collection of customer entities based on the pattern typed in the lookup field. I don’t want to write a new one, rather I’m going to reuse the QBE functionality provided in the generated CustomerList class. The new finder is just a small wrapper:

@Name("customerList")
public class CustomerList extends EntityQuery<Customer> {

	private static final String EJBQL = "select customer from Customer customer";

	private static final String[] RESTRICTIONS = {
			"lower(customer.city) like lower(concat(#{customerList.customer.city},'%'))",
			"lower(customer.code) like lower(concat(#{customerList.customer.code},'%'))",
			"lower(customer.country) like lower(concat(#{customerList.customer.country},'%'))",
			"lower(customer.name) like lower(concat(#{customerList.customer.name},'%'))",
			"lower(customer.street) like lower(concat(#{customerList.customer.street},'%'))",
			"lower(customer.zip) like lower(concat(#{customerList.customer.zip},'%'))",};
	private Customer customer = new Customer();

//...... some generated stuff removed .............

	public List<Customer> suggestByCode(Object value){
		customer = new Customer();
		customer.setCode(((String)value).toUpperCase());
		List<Customer> li = getResultList();
		return li;
	}
}

Now we can build suggestion box in the lookup field:

<s:decorate id="customerField" template="layout/edit.xhtml">
	<ui:define name="label">Customer</ui:define>
	<h:panelGrid columns="3" border="0" cellpadding="0" cellspacing="0">

		<h:panelGrid columns="2" border="0" cellpadding="0"
			cellspacing="0">
			<h:inputText id="customer" converter="#{customerConverter}"
				style="text-transform:uppercase" required="true"
				value="#{projectHome.instance.customer}" />
			<h:graphicImage value="img/arrow.png"
				onclick="#{rich:element('customer')}.value = '';#{rich:component('suggestion')}.callSuggestion(true)"
				alt="call customer list" />
		</h:panelGrid>
		<rich:spacer height="5" width="5" />
		<a:outputPanel id="customer_lov_link">
			<s:link view="/Customer.xhtml" id="customerLink"
				value="#{projectHome.instance.customer.name}" propagation="none"
				rendered="#{projectHome.managed}">
				<f:param name="customerId"
					value="#{projectHome.instance.customer.id}" />
			</s:link>
		</a:outputPanel>
	</h:panelGrid>
	<rich:suggestionbox id="suggestion" for="customer"
		suggestionAction="#{customerList.suggestByCode}" var="_cust"
		fetchValue="#{_cust.code}" nothingLabel="No Customers found">
		<a:support event="onselect" reRender="customer_lov_link"
			limitToList="true">
			<f:param name="customerId" value="#{_cust.id}" />
		</a:support>
		<h:column>
				  #{_cust.code}
			   </h:column>
		<h:column>
				  #{_cust.name}
			   </h:column>
	</rich:suggestionbox>
</s:decorate>

The most important part is green highlighted:

  • for – must match the component id of the h:inputText tag.
  • suggestionAction – contains EL called every time the suggestion box is called, either when user is typing something in the lookup field or its call by script
  • var – is the local variable containing collection elements returned by suggestionAction EL
  • fecthValue – sience our suggestionAction EL returns entities we must specify what should be put back into the lookup field after selection was made

Inside the rich:suggestionbox tag we can place a table when we need to show some additional information like description, image, etc.

Additional informations about richfaces suggestionbox could be found in RichFaces Live Demo and in the Max Katz book – Practical Richfaces. Both inspired me writing this tutorial.

The complete source code can be checked out anonymously from the subversion repository:

svn co http://svn.dobosz.at/svn/restseam/net.dobosz.restseam.server/

5 thoughts on “Build List of Values in JBoss-Seam using RichFaces suggestionbox

  1. predrag

    Perhaps everybody already figure out how to solve two shortcomings, but i’ll post how i done it just for the reference.

    1. clicking on button with arrow should list all possible options:


    where ‘customer’ in rich:element function is id of inputText component

    2. update lookup link immediately after selection new value in selection box:
    <code>

    <a4j:outputPanel id=”customer_lov_link”>
    <s:link view=”/Customer.xhtml” id=”customerLink”
    value=”#{projectHome.instance.customer.name}” propagation=”none”
    rendered=”#{projectHome.managed}”>
    <f:param name=”customerId”
    value=”#{projectHome.instance.customer.id}” />
    </s:link>
    </a4j:outputPanel>

    …….
    </code>
    and in rich:suggestion box add a4j:support
    <code>
    <rich:suggestionbox id=”suggestion” for=”customer” ….>

    <a4j:support event=”onselect” reRender=”customer_lov_link” limitToList=”true”>
    <f:param name=”customerId” value=”#{_cust.id}”/>
    </a4j:support>
    ….
    </rich:suggestionbox>
    </code>

    1. Jacek Dobosz Post author

      Thanks for the feedback. I already applied your suggestion in code and it works great!!!

  2. predrag

    finaly code in text ;), you probably can fix formating and TNX for example save me a lot of time 😉

  3. Manfred

    Hi

    how would you do it if the customer field is not required?
    If the selection of a customer is optional?

    I tried it to solve within the converter, but it doesn’t work =(

    1. Jacek Dobosz Post author

      My example covers only the basic idea, however update null values with the suggestion box is possible, but more complicated. I implemened it in another project.
      First, you need to populate null relations with empty POJO’s after loading POJO from the database. The good place is the the load() method of EnityHome. You need also override persist() and update() methods of EntiryHome in order to remove preloaded empty POJO’s if they were not updated. I will try to extract the code from my project and extend this example.

Comments are closed.