JSF dataTable

El componente h:dataTable de JavaServer Faces (JSF) se utiliza para mostrar y administrar datos tabulares en aplicaciones JSF. Proporciona una forma conveniente de representar datos en un formato de tabla con columnas y filas personalizables.

El componente dataTable le permite vincularlo a una colección de datos y genera automáticamente el HTML necesario para representar la tabla.

Puedes personalizar la apariencia del componente dataTable definiendo columnas usando el componente column y especificando los datos que se mostrarán en cada columna. El componente dataTable también proporciona soporte integrado para manejar eventos y realizar acciones en los datos.

En el siguiente ejemplo se utilizará h:dataTable para:

  • Visualizar los datos de una lista de objetos (DTO) obtenidos a través de un Managed Bean.
  • Agregar funcionalidad que permita insertar, editar y eliminar filas de la tabla.
  • Permitir que usuario pueda aplicar un ordenamiento ascendente y/o descedente por columna.

Componentes afectados

Managed Bean

El Managed Bean contiene la lista de objetos que serán visualizados por el h:dataTable y al menos su correspondiente método getter para que la vista sea accesible desde la vista. A los fines de este ejemplo los datos son cargados en la lista durante la inicialización del Managed Bean.

@Named("bean")
@ApplicationScoped
public class ExampleBean {
	private List<Contributor> contributors;
	@PostConstruct
	public void init() {
		//Inicializar datos
		this.contributors = new ArrayList<Contributor>();
		this.contributors.add(new Contributor(1, "Carl", 40, "USA", 100.0));
		this.contributors.add(new Contributor(2, "Bruce", 35, "Canada", 50.0));
		this.contributors.add(new Contributor(3, "Mark", 35, "Australia", 120.0));
		this.contributors.add(new Contributor(4, "Claire", 35, "France", 200.0));
	}
	
	public List<Contributor> getContributors() {
		return contributors;
	}
	public void setContributors(List<Contributor> contributors) {
		this.contributors = contributors;
	}
}

DTO

Los datos de cada objeto en la lista del Managed Bean corresponden a una fila del h:dataTable. En la clase declaramos las variables privadas que corresponden a cada columna del JSF h:dataTable, con sus métodos getters y setters.

public class Contributor {
	private Integer id;
	private String name;
	private String country;
	private Integer age;
	private Double contribution;
	public Contributor() {
		
	}
	
	public Contributor(Integer id, String name, Integer age, String country, Double contribution) {
		this.id = id;
		this.name = name;
		this.age = age;
		this.country = country;
		this.contribution = contribution;
	}
	public Contributor(Contributor c) {
		this.id = c.getId();
		this.name = c.getName();
		this.age = c.getAge();
		this.country = c.getCountry();
		this.contribution = c.getContribution();
	}
	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;
	}
	
	public String getCountry() {
		return country;
	}
	
	public void setCountry(String country) {
		this.country = country;
	}
	
	public Integer getAge() {
		return age;
	}
	
	public void setAge(Integer age) {
		this.age = age;
	}
	
	public Double getContribution() {
		return contribution;
	}
	
	public void setContribution(Double contribution) {
		this.contribution = contribution;
	}
}

Vista

El siguiente código muestra el fragmento de la vista en donde se declara el h:dataTable, donde cabe destacar:

  • El valor del atributo value es una expresión que hace referencia a la lista del Managed Bean.
  • h:dataTable itera la lista del Managed Bean al momento de renderizarse. El atributo var se utiliza para nombrar la variable que nos permite referir al objeto actual en la lista.
  • Los encabezados de las columnas se declaran mediante facets.
<h:dataTable 
        id="table-contributors" 
        value="#{bean.contributors}" 
        var="c"
        styleClass="table-style"
        headerClass="table-header"
        rowClasses="table-row-odd,table-row-even"
        columnClasses="table-col-1,table-col-2,table-col-3,table-col-4,table-col-5,table-col-6">
    <h:column>
        <f:facet name="header">Nombre</f:facet>
        <h:outputText value="#{c.name}" />
    </h:column>
    <h:column>
        <f:facet name="header">Edad</f:facet>
        <h:outputText value="#{c.age}" />
    </h:column>
    <h:column>
        <f:facet name="header">País</f:facet>
        <h:outputText value="#{c.country}" />
    </h:column>
    <h:column>
        <f:facet name="header">Aporte</f:facet>
        <h:outputText value="#{c.contribution}" />
    </h:column>
</h:dataTable>
JSF dataTable

Agregar un registro a JSF dataTable

Para implementar el alta de un nuevo registro crearemos un nuevo formulario en la vista.

En el Managed Bean declaramos una nueva variable de tipo Contributor (con sus getters y setters, para hacerla accesible desde la vista) que mantendrá los valores de los campos del nuevo formulario de alta. Al concretar el alta del nuevo registro el objeto se agregará a la lista mantenida en el Managed Bean.

@Named("bean")
@ApplicationScoped
public class ExampleBean {
    //...
	private Contributor contributorAddTemp;
	
    //...
	
	public Contributor getContributorAddTemp() {
		return contributorAddTemp;
	}
	public void setContributorAddTemp(Contributor contributorAddTemp) {
		this.contributorAddTemp = contributorAddTemp;
	}
    //...
	
	public String agregar() {
		this.contributorAddTemp = new Contributor();
		return "inicio";
	}
	
	public String agregarAceptar() {
		this.contributors.add(new Contributor(this.contributorAddTemp));
		this.contributorAddTemp = null;
		return "inicio";
	}
	
	public String agregarCancelar() {
		this.contributorAddTemp = null;
		return "inicio";
	}
}

En la vista los campos del nuevo formulario tienen asignada una expresión que hace referencia a la correspondiente propiedad de la nueva variable del Managed Bean (contributorAddTemp).

Notar al final del formulario los dos h:commandLink (Aceptar / Cancelar), con el atributo action asignado a sendos métodos del Managed Bean.

El método bean.agregarAceptar(), se ejecuta al confirmar el alta del nuevo registro, agrega el nuevo objeto a la lista del Managed Bean. Ahora el nuevo registro será visible en nuestra h:dataTable.

El método bean.agregarCancelar(), se ejecuta al hacer clic en el link ‘Cancelar‘. Notar el atributo immediate=»true», utilizada para saltearse las validaciones de campo requerido (required=»true»).

<!-- mas codigo -->
<h:panelGrid columns="1" columnClasses="agregar-col-0" style="width:100%;">
    <h:commandLink action="#{bean.agregar()}" value="Agregar" rendered="#{empty bean.contributorAddTemp}" />
</h:panelGrid>
<!-- mas codigo -->
<h:form id="addForm">
    <h:panelGrid columns="2" columnClasses="agregar-col-1,agregar-col-2" style="width:100%;" rendered="#{not empty bean.contributorAddTemp}">
        <h:outputText value="Nombre: " />
        <h:inputText value="#{bean.contributorAddTemp.name}" required="true" requiredMessage="Debe ingresar un nombre." />
        <h:outputText value="Edad: " />
        <h:inputText value="#{bean.contributorAddTemp.age}" required="true" requiredMessage="Debe ingresar una edad." />
        <h:outputText value="País: " />
        <h:inputText value="#{bean.contributorAddTemp.country}" required="true" requiredMessage="Debe ingresar un país." />
        <h:outputText value="Aporte: " />
        <h:inputText value="#{bean.contributorAddTemp.contribution}" required="true" requiredMessage="Debe ingresar el monto aportado." />
        <h:commandLink action="#{bean.agregarAceptar()}" value="Aceptar" />
        <h:commandLink action="#{bean.agregarCancelar()}" value="Cancelar" immediate="true" />
    </h:panelGrid>
</h:form>
<!-- mas codigo -->
JSF dataTable agregar fila

Editar o eliminar un registro en JSF dataTable

Para editar un registro de nuestra tabla necesitamos:

  1. Agregar un flag a la clase Contributor para indicar si la fila de la tabla se encuentra o no en estado editable:
public class Contributor {
    //...
	private Boolean editable;
    //...
	public Boolean getEditable() {
		return editable;
	}
	public void setEditable(Boolean editable) {
		this.editable = editable;
	}
    //...
}
  1. Agregar un editor a cada columna. Para esto usaremos un h:inputText como sigue:
<h:column>
    <f:facet name="header">Nombre</f:facet>
    <h:outputText value="#{c.name}" rendered="#{not c.editable}" />
    <h:inputText value="#{c.name}" styleClass="name-editor" rendered="#{c.editable}" />
</h:column>

Ahora cada columna cuenta con un componente de solo lectura (h:outputText) y otro editable (h:inputText). Solo uno de los dos será visible al mismo tiempo ya que esto es controlado por un flag en el objeto de datos de la fila con valores mutuamente excluyentes.

  1. Agregar una nueva columna con los comandos para cambiar el estado de la fila a editable y para guardar los datos modificados. Nuevamente, la visibilidad de estos dos componentes es mutuamente excluyente ya que depende del flag de visibilidad agregado a la clase Contributor:
<h:column>
    <f:facet name="header"></f:facet>
    <h:commandLink action="#{bean.edit(c)}" value="Editar" rendered="#{not bean.isAnyRowEditable()}" />
    <h:commandLink action="#{bean.guardar(c)}" value="Guardar" rendered="#{c.editable}" />
</h:column>

El fragmento principal de la página xhtml, que incluye los cambios explicados anteriormente queda como se muestra a continuación:

<h:form id="customerForm">
    <p />
    <h:outputText value="DataTable - Ejemplo 1:" styleClass="title" />
    <p />
    <h:dataTable 
            id="table-contributors" 
            value="#{bean.contributors}" 
            var="c"
            styleClass="table-style"
            headerClass="table-header"
            rowClasses="table-row-odd,table-row-even"
            columnClasses="table-col-1,table-col-2,table-col-3,table-col-4,table-col-5,table-col-6">
        <h:column>
            <f:facet name="header">Nombre</f:facet>
            <h:outputText value="#{c.name}" rendered="#{not c.editable}" />
            <h:inputText value="#{c.name}" styleClass="name-editor" rendered="#{c.editable}" required="true" requiredMessage="Debe ingresar un nombre." />
        </h:column>
        <h:column>
            <f:facet name="header">Edad</f:facet>
            <h:outputText value="#{c.age}" rendered="#{not c.editable}" />
            <h:inputText value="#{c.age}" styleClass="age-editor" rendered="#{c.editable}" required="true" requiredMessage="Debe ingresar una edad." />
        </h:column>
        <h:column>
            <f:facet name="header">País</f:facet>
            <h:outputText value="#{c.country}" rendered="#{not c.editable}" />
            <h:inputText value="#{c.country}" styleClass="country-editor" rendered="#{c.editable}" required="true" requiredMessage="Debe ingresar un país." />
        </h:column>
        <h:column>
            <f:facet name="header">Aporte</f:facet>
            <h:outputText value="#{c.contribution}" rendered="#{not c.editable}" />
            <h:inputText value="#{c.contribution}" styleClass="contribution-editor" rendered="#{c.editable}" required="true" requiredMessage="Debe ingresar el monto aportado." />
        </h:column>
        <h:column>
            <f:facet name="header"></f:facet>
            <h:commandLink action="#{bean.edit(c)}" value="Editar" rendered="#{not bean.isAnyRowEditable()}" />
            <h:commandLink action="#{bean.guardar(c)}" value="Guardar" rendered="#{c.editable}" />
        </h:column>
        <h:column rendered="#{not c.editable}">
            <f:facet name="header"></f:facet>
            <h:commandLink action="#{bean.delete(c)}" value="Eliminar" rendered="#{not bean.isAnyRowEditable()}" onclick="if(!confirm('Desea eliminar este registro?')) return false;" />
            <h:commandLink action="#{bean.cancelar(c)}" value="Cancelar" rendered="#{c.editable}" immediate="true" />
        </h:column>
    </h:dataTable>
    <h:panelGrid columns="1" columnClasses="agregar-col-0" style="width:100%;">
        <h:commandLink action="#{bean.agregar()}" value="Agregar" rendered="#{empty bean.contributorAddTemp}" />
    </h:panelGrid>
</h:form>
<h:form id="addForm">
    <h:panelGrid columns="2" columnClasses="agregar-col-1,agregar-col-2" style="width:100%;" rendered="#{not empty bean.contributorAddTemp}">
        <h:outputText value="Nombre: " />
        <h:inputText value="#{bean.contributorAddTemp.name}" required="true" requiredMessage="Debe ingresar un nombre." />
        <h:outputText value="Edad: " />
        <h:inputText value="#{bean.contributorAddTemp.age}" required="true" requiredMessage="Debe ingresar una edad." />
        <h:outputText value="País: " />
        <h:inputText value="#{bean.contributorAddTemp.country}" required="true" requiredMessage="Debe ingresar un país." />
        <h:outputText value="Aporte: " />
        <h:inputText value="#{bean.contributorAddTemp.contribution}" required="true" requiredMessage="Debe ingresar el monto aportado." />
        <h:commandLink action="#{bean.agregarAceptar()}" value="Aceptar" />
        <h:commandLink action="#{bean.agregarCancelar()}" value="Cancelar" immediate="true" />
    </h:panelGrid>
</h:form>

A continuación se muestran los métodos y atributo del Managed Bean utilizados para la edición y eliminación de filas:

@Named("bean")
@ApplicationScoped
public class ExampleBean {
    //...
	private Contributor temp;
    //...
	public String edit(Contributor c) {
		this.temp = new Contributor(c);
		c.setEditable(Boolean.TRUE);
		return "inicio";
	}
	public String guardar(Contributor c) {
		c.setEditable(Boolean.FALSE);
		return "inicio";
	}
	public String cancelar(Contributor c) {
		c.setAge(this.temp.getAge());
		c.setContribution(this.temp.getContribution());
		c.setCountry(this.temp.getCountry());
		c.setName(this.temp.getName());
		c.setEditable(Boolean.FALSE);
		return "inicio";
	}
	public Boolean isAnyRowEditable() {
		Optional<Contributor> contributor = 
				this.contributors.stream().filter(c -> c.getEditable() != null && c.getEditable()).findAny();
		if (contributor.isPresent()) {
			return Boolean.TRUE;
		} else {
			return Boolean.FALSE;
		}
	}
	public String delete(Contributor c) {
		this.contributors.remove(c);
		return "inicio";
	}
    //...
}
JSF dataTable editar eliminar 1
JSF dataTable editar eliminar 2

Ordenar columna en dataTable

Podemos ampliar las prestaciones de nuestra tabla dando al usuario la posibilidad de ordenar las filas de la tabla por el valor de una determinada columna. En este ejemplo vamos a implementar el ordenamiento por la columna NOMBRE.

Modificaciones en el Managed Bean

Para esto, en primer lugar vamos a crear una variable de instancia de tipo Boolean, con sus correspondientes getter y setter, que servirá de flag para permitir que el usuario pueda alternar entre un ordenamiento ascendente y descendente.

...
private Boolean sortAscFlag;
public Boolean getSortAscFlag() {
	return sortAscFlag;
}
public void setSortAscFlag(Boolean sortAscFlag) {
	this.sortAscFlag = sortAscFlag;
}
...

También a nivel de instancia vamos a crear los Comparators a utilizarse en los ordenamientos ascendente y descendente.

...
private final Comparator<Contributor> sortAsc = new Comparator<Contributor>() {
	public int compare(Contributor c1, Contributor c2) {
		return c1.getName().compareTo(c2.getName());
	}
};
private final Comparator<Contributor> sortDesc = new Comparator<Contributor>() {
	public int compare(Contributor c1, Contributor c2) {
		return c2.getName().compareTo(c1.getName());
	}
};
...

Implementaremos el método sortByName() en donde:

  • Asignaremos un valor al flag que creamos anteriormente considerando que:
    • En el primer ingreso se asigna TRUE, lo que significa que el primer ordenamiento que aplique el usuario será ascendente.
    • En los ingresos subsiguientes se invierte el valor del flag para ordenar la tabla en sentido inverso al aplicado la última vez.
  • Se realiza el ordenamiento de la lista que contiene los datos visualizados en el dataTable utilizando Collections.sort(). Notar que en el segundo parámetro se aplica el Comparator de acuerdo al valor del flag.
...
public String sortByName() {
	if (this.sortAscFlag == null) {
		this.sortAscFlag = Boolean.TRUE;
	} else {
		this.sortAscFlag = !this.sortAscFlag;
	}
	Collections.sort(this.contributors, this.sortAscFlag ? sortAsc : sortDesc);
	return "";
}
...

Modificaciones en el dataTable

En esta parte solo modificaremos la cabecera de la columna afectada (NOMBRE):

  • Agregaremos un JSF commandLink para invocar al método sortByName() que implementamos anteriormente en el Managed Bean.
  • Agregaremos un carácter especial (triángulo) para informar al usuario el sentido del ordenamiento aplicado. Se visualizará el triángulo ascendente o descendente de acuerdo al valor del flag en el Managed Bean, consultando su valor mediante una expresión en JSF Expression Language.
...
<h:column>
	<f:facet name="header">
		<h:commandLink action="#{bean.sortByName}" style="color: white;">Nombre</h:commandLink>
		<span style="#{not empty bean.sortAscFlag and bean.sortAscFlag eq true ? 'display:inline' : 'display:none'};color:white">▲</span>
		<span style="#{not empty bean.sortAscFlag and bean.sortAscFlag eq false ? 'display:inline' : 'display:none'};color:white">▼</span>
	</f:facet>
	<h:outputText value="#{c.name}" rendered="#{not c.editable}" />
	<h:inputText value="#{c.name}" styleClass="name-editor" rendered="#{c.editable}" required="true" requiredMessage="Debe ingresar un nombre." />
</h:column>
...

Aplicar filtro por columna

Aplicaremos un filtro dinámico para filas por el campo Nombre a medida que el usuario tipea el criterio de búsqueda.

En este ejemplo aplicaremos un filtro por la columna Nombre, por lo que agregaremos un h:inputText a la cabecera de dicha columna. Usaremos AJAX para hacer que el resultado del filtro se aplique mientras el usuario tipea.

<f:facet name="header">
	<h:commandLink action="#{bean.sortByName}" style="color: white;">Nombre</h:commandLink>
	<span style="#{not empty bean.sortAscFlag and bean.sortAscFlag eq true ? 'display:inline' : 'display:none'};color:white">▲</span>
	<span style="#{not empty bean.sortAscFlag and bean.sortAscFlag eq false ? 'display:inline' : 'display:none'};color:white">▼</span>
	<h:inputText value="#{bean.nameFilter}" style="margin-top: 25px;">
		<f:ajax event="keyup" execute="@this" listener="#{bean.applyFilter}" render="customerForm:table-contributors" />
	</h:inputText>
</f:facet>

Usaremos un flag para indicar qué filas deben ser visualizadas. Para esto agregaremos un atributo booleano a la entidad Contributor. Este atributo será consultado mediante una expresión EL desde la página para determinar si una fila debe ser mostrada.

Cuando se renderiza la tabla se muestra una entidad Contributor por fila, por esto es necesario inicializar el atributo displayable a TRUE en sus constructores, para que todas la filas sean visibles al momento de la carga.

public class Contributor implements Serializable {
	...
	private Boolean displayable;
	...
	public Contributor() {
		this.displayable = Boolean.TRUE;
	}
	...
	public Boolean getDisplayable() {
		return displayable;
	}
	public void setDisplayable(Boolean displayable) {
		this.displayable = displayable;
	}
}

Cuando el usuario ingresa un criterio de búsqueda en el h:inputText que agregamos en la cabecera, se captura el evento keyup y se invoca al siguiente listener mediante AJAX. En este método se implementa la lógica del filtro:

  • Si el texto ingresado es vacío, se deben visualizar todas la filas. Para esto se asigna a todas las entidades el valor TRUE al atributo displayable.
  • Si el texto ingresado no es vacío, se verifica para cada entidad si el atributo nombre comienza con los caracteres ingresados por el usuario. Si se cumple esta condición se asigna TRUE al atributo displayable.
public void applyFilter(AjaxBehaviorEvent event) {
	String input = this.nameFilter.toLowerCase().trim();
	if (input.equals("")) {
		for(Contributor c : this.contributors) {
			c.setDisplayable(Boolean.TRUE);
		}
	} else {
		for(Contributor c : this.contributors) {
			if (c.getName().toLowerCase().startsWith(input)) {
				c.setDisplayable(Boolean.TRUE);
			} else {
				c.setDisplayable(Boolean.FALSE);
			}
		}
	}
}

Por último, se modifica el criterio del atributo rendered de los componentes de cada columna del JSF dataTable para que evalúe el atributo displayable al momento de determinar si la fila debe ser visualizada.

<h:column>
	<f:facet name="header">
		<h:commandLink action="#{bean.sortByName}" style="color: white;">Nombre</h:commandLink>
		<span style="#{not empty bean.sortAscFlag and bean.sortAscFlag eq true ? 'display:inline' : 'display:none'};color:white">▲</span>
		<span style="#{not empty bean.sortAscFlag and bean.sortAscFlag eq false ? 'display:inline' : 'display:none'};color:white">▼</span>
		<h:inputText value="#{bean.nameFilter}" style="margin-top: 25px;">
			<f:ajax event="keyup" execute="@this" listener="#{bean.applyFilter}" render="customerForm:table-contributors" />
		</h:inputText>
	</f:facet>
	<h:outputText value="#{c.name}" rendered="#{c.displayable and not c.editable}" />
	<h:inputText value="#{c.name}" styleClass="name-editor" rendered="#{c.displayable and c.editable}" required="true" requiredMessage="Debe ingresar un nombre." />
</h:column>
<h:column>
	<f:facet name="header">Edad</f:facet>
	<h:outputText value="#{c.age}" rendered="#{c.displayable and not c.editable}" />
	<h:inputText value="#{c.age}" styleClass="age-editor" rendered="#{c.displayable and c.editable}" required="true" requiredMessage="Debe ingresar una edad." />
</h:column>
<h:column>
	<f:facet name="header">País</f:facet>
	<h:outputText value="#{c.country}" rendered="#{c.displayable and not c.editable}" />
	<h:inputText value="#{c.country}" styleClass="country-editor" rendered="#{c.displayable and c.editable}" required="true" requiredMessage="Debe ingresar un país." />
</h:column>
<h:column>
	<f:facet name="header">Aporte</f:facet>
	<h:outputText value="#{c.contribution}" rendered="#{c.displayable and not c.editable}" />
	<h:inputText value="#{c.contribution}" styleClass="contribution-editor" rendered="#{c.displayable and c.editable}" required="true" requiredMessage="Debe ingresar el monto aportado." />
</h:column>
<h:column>
	<f:facet name="header"></f:facet>
	<h:commandLink action="#{bean.edit(c)}" value="Editar" rendered="#{c.displayable and not bean.isAnyRowEditable()}" />
	<h:commandLink action="#{bean.guardar(c)}" value="Guardar" rendered="#{c.displayable and c.editable}" />
</h:column>
<h:column rendered="#{not c.editable}">
	<f:facet name="header"></f:facet>
	<h:commandLink action="#{bean.delete(c)}" value="Eliminar" rendered="#{c.displayable and not bean.isAnyRowEditable()}" onclick="if(!confirm('Desea eliminar este registro?')) return false;" />
	<h:commandLink action="#{bean.cancelar(c)}" value="Cancelar" rendered="#{c.displayable and c.editable}" immediate="true" />
</h:column>

Conclusión

El componente JSF dataTable puede ser una herramienta poderosa para mostrar datos relacionales en una página web. Mediante el uso de este componente, los desarrolladores pueden vincular fácilmente los datos a una tabla y personalizar su apariencia y comportamiento para satisfacer sus necesidades. Además, el componente dataTable se puede ampliar y personalizar mediante el uso de facets, lo que brinda aún más flexibilidad y control sobre su comportamiento. Con su sólido conjunto de características, el componente dataTable es un activo valioso en cualquier aplicación web basada en JSF, que facilita la visualización y manipulación eficientes de datos relacionales para satisfacer las necesidades de los usuarios.


Te puede interesar:

Arquitectura y Ciclo de Vida JSF

Cómo funciona el ciclo de vida de una aplicación JSF. En el artículo relacionado se cubre el manejo de eventos.

Seguir leyendo →