ValueChangeListener example in JSF 2

Introduction

This tutorial shows how to use ValueChangeListener to create dependent dropdown in JSF 2 and MySQL. I have used here JSF 2’s valueChangeListener which fires value change event when select option dropdown gets changed.

Any component that receives user input, such as one of the HTML select or text input components, can publish value change events.

The dependent dropdowns or cascaded dropdowns are useful when you have the requirements for selecting dropdown’s value based one another dropdown’s value. For example, you may have requirement to select country name based on continent or region.

Prerequisites

Eclipse 2020-06, Java at least 1.8, Gradle 6.5.1, Maven 3.6.3, JSF 2.2.20, MySQL 8.0.17

Project Setup

Create a gradle or maven based project in Eclipse. The name of the project is .

If you creating gradle based project then use below build.gradle script:

plugins {
	id 'war'
    id 'java-library'
}

repositories {
    jcenter()
}

dependencies {
    implementation 'com.sun.faces:jsf-api:2.2.20'
    implementation 'com.sun.faces:jsf-impl:2.2.20'
    implementation 'javax.enterprise:cdi-api:2.0.SP1'
    
    implementation 'mysql:mysql-connector-java:8.0.17'
    
    //required for JDK 9 or above
    implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
}

If you are creating maven based project then use below pom.xml file:

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.roytuts</groupId>
	<artifactId>jsf2-valuchangelistener</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	
	<dependencies>
		<dependency>
			<groupId>com.sun.faces</groupId>
			<artifactId>jsf-api</artifactId>
			<version>2.2.20</version>
		</dependency>
		
		<dependency>
			<groupId>com.sun.faces</groupId>
			<artifactId>jsf-impl</artifactId>
			<version>2.2.20</version>
		</dependency>
		
		<dependency>
			<groupId>javax.enterprise</groupId>
			<artifactId>cdi-api</artifactId>
			<version>2.0.SP1</version>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.17</version>
		</dependency>
		
		<!--required only if jdk 9 or higher version is used-->
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.4.0-b180830.0359</version>
		</dependency>
	</dependencies>
	
	<build>
		<finalName>jsf2-immediate-attribute</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>12</source>
					<target>12</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

MySQL Table Setup

We need two tables with some sample data to test the application right away once coding is finished.

CREATE TABLE IF NOT EXISTS `category` (
  `category_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `category_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  `category_link` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `parent_id` int(10) unsigned NOT NULL DEFAULT '0',
  `sort_order` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`category_id`),
  UNIQUE KEY `unique` (`category_name`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `category` (`category_id`, `category_name`, `category_link`, `parent_id`, `sort_order`) VALUES
	(1, 'Home', 'home', 0, 0),
	(2, 'Tutorials', 'tutorials', 0, 1),
	(3, 'Java', 'java', 2, 1),
	(4, 'Liferay', 'liferay', 2, 1),
	(5, 'Frameworks', 'frameworks', 0, 2),
	(6, 'JSF', 'jsf', 5, 2),
	(7, 'Struts', 'struts', 5, 2),
	(8, 'Spring', 'spring', 5, 2),
	(9, 'Hibernate', 'hibernate', 5, 2),
	(10, 'Webservices', 'webservices', 0, 3),
	(11, 'REST', 'rest', 10, 3),
	(12, 'SOAP', 'soap', 10, 3),
	(13, 'Contact', 'contact', 0, 4),
	(14, 'About', 'about', 0, 5);

CREATE TABLE IF NOT EXISTS `tutorial` (
  `category_id` int(10) unsigned NOT NULL,
  `no_of_tutorials` int(10) unsigned NOT NULL,
  KEY `fk_category_id` (`category_id`),
  CONSTRAINT `fk_category_id` FOREIGN KEY (`category_id`) REFERENCES `category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `tutorial` (`category_id`, `no_of_tutorials`) VALUES
	(2, 3),
	(3, 2),
	(5, 1),
	(6, 5),
	(7, 3),
	(8, 4);

Deployment Descriptor

Create web.xml file under src/main/webapp/WEB-INF folder with below the below content to define JSF 2 related configurations.

<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
	id="WebApp_ID" version="3.0">

	<display-name>JSF 2.2 ValueChangeListener Example</display-name>

	<context-param>
		<param-name>javax.faces.PROJECT_STAGE</param-name>
		<param-value>Development</param-value>
	</context-param>

	<servlet>
		<servlet-name>Faces Servlet</servlet-name>
		<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>Faces Servlet</servlet-name>
		<url-pattern>*.jsf</url-pattern>
	</servlet-mapping>

	<servlet-mapping>
		<servlet-name>Faces Servlet</servlet-name>
		<url-pattern>*.faces</url-pattern>
	</servlet-mapping>

	<servlet-mapping>
		<servlet-name>Faces Servlet</servlet-name>
		<url-pattern>*.xhtml</url-pattern>
	</servlet-mapping>

	<welcome-file-list>
		<welcome-file>index.jsf</welcome-file>
	</welcome-file-list>
</web-app>

Database Constants

I have put the database configuration values into class as constant values.

package com.roytuts.jsf2.valuchangelistener;

public final class Constants {

	private Constants() {}
	
	public static final String DB_USER_NAME = "root";
    public static final String DB_USER_PASSWORD = "root";
    public static final String DB_INSTANCE_NAME = "roytuts";
    public static final String DB_CONNECTION_URL = "jdbc:mysql://localhost:3306";
    public static final String DB_DRIVER_CLASS = "com.mysql.cj.jdbc.Driver";
	
}

Database Helper

I have created the following database helper class that will be used to query database table. Instead of writing the same piece of code every time while executing queries, I am putting into a helper class to reuse wherever required.

package com.roytuts.jsf2.valuchangelistener;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public final class DBHelper {

	private DBHelper() {
	}

	/**
	 * to load the database driver
	 *
	 * @return a database connection
	 * @throws SQLException throws an exception if an error occurs
	 */
	public static Connection getDBConnection() throws SQLException {
		Connection conn = null;
		try {
			Class.forName(Constants.DB_DRIVER_CLASS);
			conn = DriverManager.getConnection(Constants.DB_CONNECTION_URL + "/" + Constants.DB_INSTANCE_NAME,
					Constants.DB_USER_NAME, Constants.DB_USER_PASSWORD);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return conn;
	}

	/**
	 * to get a result set of a query
	 *
	 * @param query custom query
	 * @return a result set of custom query
	 * @throws SQLException throws an exception if an error occurs
	 */
	public static ResultSet getDBResultSet(Connection conn, String query) throws SQLException {
		ResultSet rs = null;
		if (null != conn) {
			PreparedStatement st = conn.prepareStatement(query);
			rs = st.executeQuery();
		}
		return rs;
	}

	/**
	 * to run an update query such as update, delete
	 *
	 * @param query custom query
	 * @throws SQLException throws an exception if an error occurs
	 */
	public static void runQuery(Connection conn, String query) throws SQLException {
		if (null != conn) {
			PreparedStatement st = conn.prepareStatement(query);
			st.executeUpdate();
		} else {
			System.out.println("Query execution failed!");
		}
	}

	/**
	 * close an opened PreparedStatement
	 *
	 * @return a void
	 * @throws SQLException throws an exception if an error occurs
	 */
	public static void closePreparedStatement(PreparedStatement ps) throws SQLException {
		if (null != ps) {
			ps.close();
		}
	}

	/**
	 * close an opened ResultSet
	 *
	 * @return a void
	 * @throws SQLException throws an exception if an error occurs
	 */
	public static void closeResultSet(ResultSet rs) throws SQLException {
		if (null != rs) {
			rs.close();
		}
	}

	/**
	 * close an opened database connection
	 *
	 * @return a void
	 * @throws SQLException throws an exception if an error occurs
	 */
	public static void closeDBConnection(Connection conn) throws SQLException {
		if (null != conn) {
			conn.close();
		}
	}

}

Query Helper

Query helper class returns the result set required for the application.

package com.roytuts.jsf2.valuchangelistener;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class QueryHelper {

	public List<Category> getAllCategories() {
		Connection connection = null;

		List<Category> categories = new ArrayList<>();

		Category emptyCat = new Category();
		emptyCat.setId(0);
		emptyCat.setName("-- Select Category --");

		categories.add(emptyCat);

		try {
			connection = DBHelper.getDBConnection();
			if (connection != null) {
				String sql = "SELECT category_id, category_name FROM category WHERE parent_id=0";
				ResultSet resultSet = DBHelper.getDBResultSet(connection, sql);

				if (resultSet != null) {
					while (resultSet.next()) {
						Category category = new Category();
						category.setId(resultSet.getInt(1));
						category.setName(resultSet.getString(2));
						categories.add(category);
					}
					return categories;
				}
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				if (connection != null) {
					DBHelper.closeDBConnection(connection);
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		return null;
	}

	public List<Category> getAllSubCategories(Integer category_id) {
		Connection connection = null;
		List<Category> categories = new ArrayList<>();

		Category emptyCat = new Category();
		emptyCat.setId(0);
		emptyCat.setName("-- Select Sub-category --");

		try {
			connection = DBHelper.getDBConnection();
			if (connection != null) {
				String sql = "SELECT category_id, category_name FROM category WHERE parent_id=" + category_id;
				ResultSet resultSet = DBHelper.getDBResultSet(connection, sql);
				if (resultSet != null) {
					categories.add(emptyCat);
					while (resultSet.next()) {
						Category category = new Category();
						category.setId(resultSet.getInt(1));
						category.setName(resultSet.getString(2));
						categories.add(category);
					}
					return categories;
				}
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				if (connection != null) {
					DBHelper.closeDBConnection(connection);
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		return null;
	}

	public Integer getNoOfTutorials(Integer category_id) {
		Connection connection = null;
		try {
			connection = DBHelper.getDBConnection();
			if (connection != null) {
				String sql = "SELECT no_of_tutorials FROM tutorial WHERE category_id=" + category_id;
				ResultSet resultSet = DBHelper.getDBResultSet(connection, sql);
				if (resultSet != null) {
					if (resultSet.next()) {
						Integer no = resultSet.getInt(1);
						return no;
					}
				}
			}
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			try {
				if (connection != null) {
					DBHelper.closeDBConnection(connection);
				}
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}
		return null;
	}

}

Model Class

You need to create a model class to map the database table column values with Java attributes.

package com.roytuts.jsf2.valuchangelistener;

public class Category {

	private Integer id;
	private String name;

	//getters and setters

}

JSF Managed Bean

Now you need JSF Managed Bean class to perform the business logic. You can also perform business validation in this class. You need to bind any values on the UI with this managed bean.

I am not using faces-config.xml file to configure the managed bean, rather I am using annotation based configurations.

package com.roytuts.jsf2.valuchangelistener;

import java.io.Serializable;
import java.util.List;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import javax.faces.event.ValueChangeEvent;

@ViewScoped
@ManagedBean
public class DependentDropdownMBean implements Serializable {

	private static final long serialVersionUID = 1L;

	private Integer category;
	private Integer subCategory;
	private Integer noOfTutorials;
	private List<Category> categories;
	private List<Category> subCategories;
	private QueryHelper queryHelper;

	public DependentDropdownMBean() {
		queryHelper = new QueryHelper();
	}

	public Integer getCategory() {
		return category;
	}

	public void setCategory(Integer category) {
		this.category = category;
	}

	public Integer getSubCategory() {
		return subCategory;
	}

	public void setSubCategory(Integer subCategory) {
		this.subCategory = subCategory;
	}

	public Integer getNoOfTutorials() {
		populateNoOfTutorials();
		return noOfTutorials;
	}

	public void setNoOfTutorials(Integer noOfTutorials) {
		this.noOfTutorials = noOfTutorials;
	}

	public List<Category> getCategories() {
		if (categories == null) {
			populateCategories();
		}
		return categories;
	}

	public void setCategories(List<Category> categories) {
		this.categories = categories;
	}

	public List<Category> getSubCategories() {
		populateSubCategories();
		return subCategories;
	}

	public void setSubCategories(List<Category> subCategories) {
		this.subCategories = subCategories;
	}

	private void populateCategories() {
		categories = queryHelper.getAllCategories();
	}

	private void populateSubCategories() {
		if (category != null) {
			subCategories = queryHelper.getAllSubCategories(category);
		}
	}

	private void populateNoOfTutorials() {
		if (subCategory != null) {
			noOfTutorials = queryHelper.getNoOfTutorials(subCategory);
		}
	}

	// when category selection gets changed
	public void onCategorySelect(ValueChangeEvent vce) {
		System.out.println("onCategorySelect => vce.getNewValue().toString(): " + vce.getNewValue().toString());
		Integer newCat = Integer.valueOf(vce.getNewValue().toString());
		if (newCat != category) {
			setCategory(newCat);
		}
	}

	// when sub-category selection gets changed
	public void onSubcategorySelect(ValueChangeEvent vce) {
		System.out.println("onSubcategorySelect => vce.getNewValue().toString(): " + vce.getNewValue().toString());
		Integer newSubcat = Integer.valueOf(vce.getNewValue().toString());
		if (newSubcat != subCategory) {
			setSubCategory(newSubcat);
		}
	}

}

UI – View File

Finally I have created the below index.xhtml file which will be rendered on UI (User Interface) to display the required data.

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:h="http://xmlns.jcp.org/jsf/html"
	xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
	<title>JSF 2.2 Dependent Dropdown (ValueChangeListener) Example</title>
</h:head>
<h:body>
	<h2>Select and see the results</h2><br/>
	<h:form prependid="false">
		<!-- html table having only one column -->
		<h:panelGrid columns="1">
			<!-- check category list not empty -->
			<h:panelGroup rendered="#{!empty dependentDropdownMBean.categories}">
				Category:
				<h:selectOneMenu value="#{dependentDropdownMBean.category}"
					onchange="submit()"
					valueChangeListener="#{dependentDropdownMBean.onCategorySelect}">
					<f:selectItems value="#{dependentDropdownMBean.categories}" var="cat"
						itemLabel="#{cat.name}" itemValue="#{cat.id}"></f:selectItems>
				</h:selectOneMenu>
			</h:panelGroup>
			<!-- check sub-category list not empty -->
			<h:panelGroup rendered="#{!empty dependentDropdownMBean.subCategories}">
				Sub-category:
				<h:selectOneMenu value="#{dependentDropdownMBean.subCategory}"
					onchange="submit()"
					valueChangeListener="#{dependentDropdownMBean.onSubcategorySelect}">
					<f:selectItems value="#{dependentDropdownMBean.subCategories}" var="subCat"
						itemLabel="#{subCat.name}" itemValue="#{subCat.id}"></f:selectItems>
				</h:selectOneMenu>
			</h:panelGroup>
			<!-- if number of tutorial found for the category/subcategory then print -->
			<h:panelGroup rendered="#{dependentDropdownMBean.noOfTutorials gt 0}">
				No. of Tutorials: #{dependentDropdownMBean.noOfTutorials}
			</h:panelGroup>
			<!-- if number of tutorial not found for the category/subcategory then print -->
			<h:panelGroup rendered="#{dependentDropdownMBean.noOfTutorials lt 0}">
				No tutorial found for this category!
			</h:panelGroup>
		</h:panelGrid>
	</h:form>
</h:body>
</html>

Testing the Application

You need to deploy the application into Tomcat server before you test your application.

Once your application is deployed you can hit the URL http://localhost:8080/jsf2-valuchangelistener/ in the browser and you will see the below output:

jsf 2 valuechangelistener

Next I select Tutorials from the Category dropdown and Java from the Sub-category dropdown and I get the following result:

jsf 2 valuechangelistener

That’s all about ValueChangeListener example in JSF 2.

Source Code

Download

Thanks for reading.

Leave a Reply

Your email address will not be published. Required fields are marked *