Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use <h:selectBooleanCheckbox> in <h:dataTable> or <ui:repeat> to select multiple items?

I have a Facelets page with a <h:dataTable>. In each row there is a <h:selectBooleanCheckbox>. If the checkbox is selected the object behind the corresponding row should be set in the bean.

  1. How do I do this?
  2. How to get the selected rows or their data in a backing bean?
  3. Or would it be better to do it with <h:selectManyCheckbox>?
like image 841
c0d3x Avatar asked Mar 26 '10 15:03

c0d3x


3 Answers

Your best bet is to bind the <h:selectBooleanCheckbox> value with a Map<Item, Boolean> property where Item represents the object behind the corresponding row.

<h:dataTable value="#{bean.items}" var="item">
    <h:column>
        <h:selectBooleanCheckbox value="#{bean.checked[item]}" />
    </h:column>
    ...
</h:dataTable>
<h:commandButton value="submit" action="#{bean.submit}" />
public class Bean {
    private Map<Item, Boolean> checked = new HashMap<Item, Boolean>();
    private List<Item> items;

    public void submit() {
        List<Item> selectedItems = checked.entrySet().stream()
            .filter(Entry::getValue)
            .map(Entry::getKey)
            .collect(Collectors.toList());

        checked.clear(); // If necessary.

        // Now do your thing with selectedItems.
    }

    // ...
}

You see, the map is automatically filled with all table items as key and the checkbox value is automatically set as map value associated with the item as key.

This only requires that the Item#equals() and Item#hashCode() is properly implemented as per their contracts.

If you can't guarantee that, then you'd better use a Map<RowId, Boolean> instead where RowId represents the type of the row identifier. Let's take an example that you've a Item object whose identifier property id is a Long:

public class Item {
    private Long id;
    // ...
}
<h:dataTable value="#{bean.items}" var="item">
    <h:column>
        <h:selectBooleanCheckbox value="#{bean.checked[item.id]}" />
    </h:column>
    ...
</h:dataTable>
<h:commandButton value="submit" action="#{bean.submit}" />
public class Bean {
    private Map<Long, Boolean> checked = new HashMap<Long, Boolean>();
    private List<Item> items;

    public void submit() {
        List<Item> selectedItems = items.stream()
            .filter(item -> checked.get(item.getId()))
            .collect(Collectors.toList());

        checked.clear(); // If necessary.

        // Now do your thing with selectedItems.
    }

    // ...
}
like image 172
BalusC Avatar answered Nov 02 '22 23:11

BalusC


In the following example I am using checkboxes to select two or more products to allow the user to compare product specifications on a new web page using JSF 2.0.

It took me a good while to find the following problem (totally obvious now of course) so thought it worth a mention for those trying to use pagination with BalusC's code above (nice answer BalusC, much simpler than I ever imagined it would be).

If you are using pagination you will get nullpointers at the line:

if (checked.get(item.getId()))

-in BalusC's code above.

This is because only displayed check boxes are added to the Map (doh; slap forehead). For those products whose check boxes are never displayed, due to pagination, this line will result in a null pointer error and a check needs to be added to ignore these null pointers (assuming that all check boxes are unchecked on page load). In order for the user to tick a check box then they need to display the pagination page so all works well there after.

If some or all of the check boxes are required to be ticked on first page load then this will be of no help to you...you will have to manually add those to the Map in order for them to be displayed correctly on page load.

Note: because I am using a JPA 'Entity class from database' object I also needed to use @Transient for the id in my ProductTbl Entity Class as all variables are considered columns in the database by JPA, by default, unless prefixed with @Transient. Also I am using a second link to reset the check boxes, which calls clearSelections(), and my 'submit' is a link calling compareSelectedProducts() rather than a Submit button.

The full code is as follows:

In the 'ProductTbl' Entity class derived from the database :

@Transient
private Long id;

public Long getId()
{
    return id;
}

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

In the backing bean 'ProductSelection':

private Map<Long, Boolean> checked = new HashMap<Long, Boolean>();
private String errorMessage = "";
// List of all products.
private List<ProductTbl> products;
// List of products to compare.
private List<ProductTbl> compareProducts;

// Setters and getters for above...

public String compareSelectedProducts() 
{
    // Reset selected products store.
    compareProducts = new ArrayList();

    for (ProductTbl item: products) 
    {
        // If there is a checkbox mapping for the current product then...
        if(checked.get(item.getId()) != null)
        {
           // If checkbox is ticked then...
           if (checked.get(item.getId())) 
            {
                // Add product to list of products to be compared.
                compareProducts.add(item);
            } 
        }
    }

    if(compareProducts.isEmpty())
    {
        // Error message that is displayed in the 'ErrorPage.xhtml' file.
        errorMessage = "No Products selected to compare specifications. Select two or more products by ticking the check box in the second column 'Cmpr'";
        return "process_ErrorPage";
    }

    // Rest of code to get product specification data ready to be displayed.

    return "process_CompareSelected";
}

public String clearSelections()
{
    // Untick all checkbox selections.
    checked.clear();

    return "process_MainSearchResult";
}

In the JSF Web page 'MainSearchResult.xhtml':

<h:commandLink action="#{productSelection.compareSelectedProducts()}" value="Cmpr Specification Comparison Table" /> 
<h:commandLink action="#{productSelection.clearSelections()}" value="Clear Selected" />

<h:dataTable value="#{productSelection.products}" rows="#{productSelection.numberRowsToDisplay}" first="#{productSelection.rowStart}" var="item" headerClass="table-header" >
    <h:column>
       <f:facet name="header">
          <h:outputText style="font-size:12px" value="Cmpr" />
       </f:facet>
       <div style="text-align:center;" >
          <h:selectBooleanCheckbox value="#{productSelection.checked[item.id]}" />
       </div>
    </h:column>
</h:dataTable>

In the 'faces-config.xml' file:

<navigation-rule>
    <navigation-case>
        <from-outcome>process_MainSearchResult</from-outcome>
        <to-view-id>/MainSearchResult.xhtml</to-view-id>
    </navigation-case>
</navigation-rule>
<navigation-rule>
    <navigation-case>
        <from-outcome>process_CompareSelected</from-outcome>
        <to-view-id>/CompareSelected.xhtml</to-view-id>
    </navigation-case>
</navigation-rule>
<navigation-rule>
    <navigation-case>
        <from-outcome>process_ErrorPage</from-outcome>
        <to-view-id>/ErrorPage.xhtml</to-view-id>
    </navigation-case>
</navigation-rule>
like image 4
Robbie62 Avatar answered Nov 02 '22 21:11

Robbie62


One way to send in a parameter via <h:selectBooleanCheckbox> is to send it in via the title of the Checkbox. In the ValueChangeListener, you can get it from the component using a getAttributes().get("title"). This helps in cases where you want to send an id value as a parameter (as opposed to the selected row index).

like image 1
Mallika Avatar answered Nov 02 '22 22:11

Mallika