AJAX Magic With JSF DataTables in Seam 2.0

<Seam 2.0 on Tomcat … | Seam Portlet Bridge Released >
RichFaces 3.1.3.GA came with a some new controls, and the <rich:listShuttle/> was of some interest to me. However, for a particular use case I found it not sufficient for my needs, and had to roll out my own version of it with a little bit of Ajax to add.

Let me use this simple scenario to display what I needed to do. Assuming you were keeping a shopping cart. After users select an item they want to buy, you want them to specify the quantity of that item following which their total charge is calculated for them on the fly. This I thought of doing using two DataTables just like the <rich:listShuttle/> appears but on a sleeker (or is it cruder?) level.

I defined an interface with a simple set of methods which I felt would do the trick called InPlaceEditing.

public interface InPlaceEditing {

void editSelection();

void addSelection();

void removeSelection();

void cancelSelection();

}

Here is my”shopper” Seam component which implements the interface. Notice the use of 2 DataModels “products” and “selectedItems” and their corresponding DataModelSelections. This is to enable me select from one table to another.

@Name(“shopper”)

@Scope(ScopeType.CONVERSATION)

public class Shopper implements java.io.Serializable, InPlaceEditing {

private boolean edit;

@In

EntityManager entityManager;

@In

FacesMessages facesMessages;

 

@DataModel

List<Product> products;

 

@DataModelSelection(“products”)

private Product product;

 

@DataModel

private List<CartItem> selectedItems;

 

@DataModelSelection(“selectedItems”)

private CartItem selectedItem;

 

@Out(required = false)

private CartItem cartItem;

 

private BigDecimal total = new BigDecimal(0.0);

 

private static int count = 0;

 

@Begin(flushMode=FlushModeType.MANUAL)

public void beginShopping() {

products = entityManager.createQuery(“Select p from Product p”).getResultList();

selectedItems = new ArrayList<CartItem>();

}

The beginShopping() method starts a conversation and FlushMode is set to MANUAL. This means that all changes made to entities will be made persistent only if I call flush() on entityManager. Seam defaults to AUTO which means all changes to all managed entities are merged into the persistence context after every Seam action call. Trust me, for the purposes of this trick, you DON’T want automatic persistence context merging! Anyway this action populates the “products” DataModel with products already entered into the database. The result is the table below.

<a:outputPanel id=”productPanel”>

<rich:dataTable value=”#{products}” var=”product”>

<h:column>

<f:facet name=”header”>Product</f:facet>

<a:commandLink value=”#{product.name}” actionListener=”#{shopper.editSelection}” reRender=”editPanel”/>

</h:column>

<h:column>

<f:facet name=”header”>Price</f:facet>

#{product.price}

</h:column>

<h:column>

<f:facet name=”header”>Stock</f:facet>

#{product.stock}

</h:column>

</rich:dataTable>

</a:outputPanel>

Displayed editing panel

A look at the section of the shop.xhtml facelet shows an <a:commandLink/> – an Ajaxified version of the <h:commandButon/>. This supplies our “shopper” component with the selected product through the “product” data model selection through Ajax. Also take note of the “reRender” and <a:outputPanel/> tags, which specify which areas of our page should be Ajax refreshed after certain actions are called. In the editSelection() action a new CartItem (a join entity between a product and a shopping cart) containing the selected product is created and outjected.

public void editSelection() {

if (edit == false) {

cartItem = new CartItem();

cartItem.setId(count++);

cartItem.setProduct(product);

cartItem.setSelected(true);

return;

}

cartItem = selectedItem;

cartItem.setSelected(true);

edit = true;

}

I want to use the same action to represent selecting a new product (from “products”) and editing an already existing CartItem (from “selectedItems”). This is achieved with a private “edit” boolan field, which by default is false. Also since proper behaviour of DataModel and DataModelSelections depends on a properly implemented equals() method (i.e. instance uniqueness) I assigned artificial ids with the static “count” to each CartItem. Note that without setting flushMode to MANUAL, doing this will cause exceptions to be thrown down the lane somewhere. Finally setting the CartItem’s “selected” field to true enables the rendering of the panel in which the quantity of that product will be entered.

Displayed editing panel

 

DataTable with configured product

 

Once the quantity is specified, the addSelection() action is called. Because of the boolean “edit” we are able to determine if this is a new product selection or one from the existing list and respond appropriately, calculating the total cost of the users purchases so far and modifying the corresponding DataModels appropriately. In the case of an edit, the old computed cost of a product is deducted from the total, and re-computed and added to the total. To guarantee that the change in costs will reflect properly, the edited “cartItem” is removed and re-inserted in to the “selectedItems” DataModel.

It is also very easy to remove an already configured CartItem, again with the combination of <a:commandLink/> and the “selectedItem” DataModelSelection.

public void removeSelection() {

total.subtract(selectedItem.getCost());

selectedItems.remove(selectedItem);

}

Finally, the user may save their selection. This involves the creation of a new Cart object, which is persisted to get an ID and associated with the CartItems in the “selectedItems” DataModel. Note here that the fake Ids I generated for the CartItems will cause maximum trouble in the database, and so I set them to null to force the persistence context to generate proper Ids for them. Don’t forget the all important “entityManager.flush()” to make all changes permanent.

Completed Shopping

Finally I raise an event (“shopper.events.CartEdit”) which is being observed by the “shopList” Seam component’s list() action causing the “carts” DataModel to be refreshed with fresh data.

 

@Factory(“carts”)

@Observer(“shopper.events.CartEdited”)

public void list(){

Contexts.getSessionContext().set(“carts”, null);

carts = entityManager.createQuery(“Select c from Cart c”).getResultList();

}

Carts DataTable displaying products selected

The Job Is Done. This is use of Ajax and DataTables is quite simplistic, but I’ve used it in some really tight corners for some advance stuff. One scenario that I can see with this example is the requirement to remove a product from the list of products once selected and configured, or to put it back once it has been removed from the configured ones. Another will be how to edit an already persisted cart to remove or add products to that cart. I’ve just laid the foundation. With some tricks of your own, you should be able to achieve some serious magic.