You are viewing a plain text version of this content. The canonical link for it is here.
Posted to users@tapestry.apache.org by Adam Zimowski <zi...@gmail.com> on 2011/03/25 20:29:19 UTC

T5: form validation with pre-existing errors

I have a shopping cart. The cart is built with a single form, and a
loop. The basic validation works, such as testing quantity for numeric
input etc. However, I have an additional requirement to display errors
on invalid line items which are part of the cart. An example of
invalid cart item may be wrong quantity (sold in lots of 10, but only
7 added to the cart). For the purposes of this discussion, assume that
quantity can only be odd. It is a business requirement that this check
is not done at the time of adding a product to the cart, but during
cart display.

On this mailing list, I found Josh Canfield's example of AttachError
mixin, which I implemented and it almost works, but with few wrinkles.
Here is my code:

@MixinAfter
public class AttachError {
	
	@Parameter(required = true, allowNull = true)
	private String message;

	@Environmental
	private ValidationTracker tracker;

	@InjectContainer
	private Field field;

	
	void setupRender() {
		if (message != null) {
			tracker.recordError(field, message);
		}
	}
}

public class ShoppingCart extends BasePage {
	
	@Inject
	private Logger log;

	@Inject
	private ShoppingCartServiceRemote cartService;

	@Property
	private CartItemDisplayBean cartDisplayItem;
	
	@Property
	private List<CartItemDisplayBean> cartDisplayItems;
	
	@Environmental
	private ValidationTracker tracker;

	@Component(id="quantity", parameters = {"AttachError.message=fieldError"})
	@MixinClasses(value=AttachError.class)
	private TextField quantityField;

	@Property
	private Integer indexer;


	public String getFieldError() {
		String error = null;
		if(tracker.getError(quantityField) != null) return null;
		if(cartDisplayItem.getQuantity()%2 == 0) {
			// simple example of preexisting error; don't allow even quantity
			error = "[" + cartDisplayItem.getSku() + "] Quantity must be odd";
		}

		return error;
	}
	
	@OnEvent(value=EventConstants.PREPARE, component="cartForm")
	void prepareCartData() {
		cartDisplayItems = getShoppingCartForDisplay();
	}
	
	private List<CartItemDisplayBean> getShoppingCartForDisplay() {
  // retrieve shopping cart (using ShoppingCartServiceRemote EJB)
 }
}

public class CartItemDisplayBean implements Comparable<CartItemDisplayBean> {

	private int lineNumber;
	
	private String sku;
	
	private String description;
	
	private String usuallyShipsMessage;
	
	private Integer quantity;
	
	private Double price;
	
	private Double total;
	
	private boolean deleted;

	public String getSku() {
		return sku;
	}

	public void setSku(String aSku) {
		sku = aSku;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String aDescription) {
		description = aDescription;
	}

	public String getUsuallyShipsMessage() {
		return usuallyShipsMessage;
	}

	public void setUsuallyShipsMessage(String aUsuallyShipsMessage) {
		usuallyShipsMessage = aUsuallyShipsMessage;
	}

	public Integer getQuantity() {
		return quantity;
	}

	public void setQuantity(Integer aQuantity) {
		quantity = aQuantity;
	}

	public Double getPrice() {
		return price;
	}

	public void setPrice(Double aPrice) {
		price = aPrice;
	}

	public Double getTotal() {
		return total;
	}

	public void setTotal(Double aTotal) {
		total = aTotal;
	}

	public int getLineNumber() {
		return lineNumber;
	}

	public void setLineNumber(int aLineNumber) {
		lineNumber = aLineNumber;
	}

	public boolean isDeleted() {
		return deleted;
	}

	public void setDeleted(boolean aDeleted) {
		deleted = aDeleted;
	}

	@Override
	public int compareTo(CartItemDisplayBean aAnotherBean) {
		return lineNumber - aAnotherBean.getLineNumber();
	}
}

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
t:title="${pageTitle}" xmlns:p="tapestry:parameter">
<h1>Shopping Cart</h1>
<style type="text/css">
td.kk-currency-cell {
 text-align: right;
}
</style>

<t:form t:id="cartForm">
<t:errors/>
<table border="1" width="100%" cellpadding="3">
<tr>
<th>${message:lineNumber-label}</th>
<th>${message:sku-label}</th>
<th>${message:description-label}</th>
<th>${message:usuallyShipsMsg-label}</th>
<th>${message:quantity-label}</th>
<th>${message:price-label}</th>
<th>${message:total-label}</th>
<th width="140px"/>
</tr>
<tr t:type="Loop" t:source="cartDisplayItems"
t:value="cartDisplayItem" t:index="indexer" t:formstate="ITERATION">
 <td>${cartDisplayItem.lineNumber}</td>
 <td><t:pagelink page="searchResults"
context="cartDisplayItem.sku">${cartDisplayItem.sku}</t:pagelink></td>
 <td>${cartDisplayItem.description}</td>
 <td>${cartDisplayItem.usuallyShipsMessage}</td>
 <td><t:textfield t:id="quantity" value="cartDisplayItem.quantity"
label="prop:quantityLabel" validate="required" size="2"/></td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency" value="cartDisplayItem.price"/></td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency" value="cartDisplayItem.total"/></td>
 <td>
  <t:submit t:id="update" value="message:update-value" context="indexer"/>
  <t:submit t:id="remove" value="message:remove-value" context="indexer"/>
 </td>
</tr>
<tr>
 <td colspan="4"></td>
 <td>Subtotal:</td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency" value="shoppingCart.cartSubTotal"/></td>
</tr>
<tr>
 <td colspan="4"></td>
 <td>Promo:</td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency"
value="shoppingCart.cartDiscountAmount"/></td>
</tr>
<tr>
 <td colspan="4"></td>
 <td>Total:</td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency" value="shoppingCart.cartTotal"/></td>
</tr>
</table>
<t:errors/>
</t:form>

</t:layout>

Pre-conditional errors are properly displayed, but with some issues:

1) <t:errors/> renders differently if it's located at the top of the
form versus at the bottom. If placed at the top, it renders errors on
every other page request. Literally, clicking same "Shopping Cart"
page link will once display errors, then hide them, then display them
again and so on.. When placed at the bottom of the form, errors are
displayed every time.

2) When I enter invalid quantity (such as empty or non-numeric value)
triggering normal form validation, the additional error is correctly
displayed (if <t:errors/> is placed at the bottom). However, as with a
normal form behavior, if I click the shopping cart link page, I expect
the invalid quantity error to be reset (removed). As it turns out on
the very first request of the page that error remains, and only on the
second request it is removed. I need it to be gone right away if user
navigates away from the page or re-request it by clicking the link.

So, this brings me to my questions:

1) How can I get <t:errors/> to work equally well at the top of the
form and at the bottom? I tried delegating it using <t:delegate> to a
block that's at the bottom of the form but same incorrect behavior was
observed.

2) How can I get trivial validation errors to go away right on a
subsequent first page reqest, such as when clicking on shopping cart
link to re-render the page.

Adam

---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscribe@tapestry.apache.org
For additional commands, e-mail: users-help@tapestry.apache.org


Re: T5: form validation with pre-existing errors

Posted by Taha Hafeez <ta...@gmail.com>.
ValidationTracker !! Thanks for sharing

regards
Taha


On Thu, Mar 31, 2011 at 1:06 AM, Adam Zimowski <zi...@gmail.com> wrote:

> Tapestry friends!
>
> Your silence combined with my milestone demo today only motivated me
> to stay up couple of nights in a row, fire off debugger and step into
> Tapestry source to get this solved. And so, amazing what can be done
> under the pressure from management and when your job possibly may be
> on the line...
>
> In any case, my project is about 60% along the way, on target for go
> live in Feb 2012. I now have a fully working advanced shopping cart. I
> am sharing the code below for sake of completness of this thread, but
> since my service layer runs the EJBs, the backend details are left
> out. If there is interest, I can write a wiki page on advanced
> shopping cart, covering the following features:
>
> * pre existing errors (persisted by the ejb)
> * form submission errors
> * multiple form design (vs. single form)
> * integrated use of Josh's (Canfield) AttachError mixin
> * integrated use of indexed trackers
>
> As a reminder, this shopping cart mimics functional behavior of our
> current cart website: www.chdist.com , www.avenuesupply.ca and
> www.industrialsupplies.com
>
> Here is the code:
>
> /**
>  * Controls display and processing of data on the shopping cart page.
> Delegates
>  * all business logic to the service layer.
>  *
>  * @author Adam Zimowski
>  */
> public class ShoppingCart extends BasePage {
>
>         // component id's used within this class
>        public static final String ID_CART_FORM = "cartForm";
>        public static final String ID_QUANTITY_FIELD = "quantity";
>        public static final String ID_REMOVE_LINK = "remove";
>
>        @Inject
>        private Logger log;
>
>        @Inject
>         private CatalogServiceRemote catalogService;
>
>        @Inject
>        private ShoppingCartServiceRemote cartService;
>
>        @Property
>        private CartItemDisplayBean cartDisplayItem;
>
>         @Component(id=ID_QUANTITY_FIELD, parameters =
> {"AttachError.message=fieldError"})
>        @MixinClasses(value=AttachError.class)
>        private TextField quantityField;
>
>         @Component(parameters = {"tracker=tracker"})
>        private Form cartForm;
>
>        @Persist(PersistenceConstants.FLASH)
>        private Map<Integer, ValidationTracker> indexedTrackers;
>
>        @Property
>        private int index;
>
>        private int submittedQuantity;
>
>        private int submittedLineNumber;
>
>        private String submittedSku;
>
>
>        public ValidationTracker getTracker() {
>                if(indexedTrackers == null) return new
> ValidationTrackerImpl();
>                return indexedTrackers.get(index);
>        }
>
>        public void setTracker(ValidationTracker aTracker) {
>
>                if(indexedTrackers == null) {
>                        if(log.isTraceEnabled()) log.trace("crating indexed
> trackers map");
>                        indexedTrackers = new HashMap<Integer,
> ValidationTracker>();
>                }
>
>                if(log.isTraceEnabled()) {
>                        log.trace("setting tracker for index: " + index);
>                }
>
>                indexedTrackers.put(index, aTracker);
>         }
>
>        public String getFieldError() {
>                String error = null;
>                 ValidationTracker tracker = getTracker();
>                if(tracker != null && tracker.getError(quantityField) !=
> null) {
>                        return null;
>                }
>                CartItemBean cib =
> findCartItem(cartDisplayItem.getLineNumber());
>                if(!cib.isValid()) {
>                        List<MessageHolderBean> lineItemErrors =
> cib.getErrorMessages();
>                        int counter;
>                        for(counter = 0; counter < lineItemErrors.size();
> ++counter) {
>                                MessageHolderBean lineItemError =
> lineItemErrors.get(counter);
>                                String liErrId =
> lineItemError.getMessageId();
>                                // for now we only pull out the first error;
> in the future we
>                                // may need to account for the entire
> collection. As of 4.0,
>                                // collection always contains only 1 item
>                                if(counter == 0) {
>                                        String liErrTemplate =
> getMessages().get(liErrId);
>                                        List<Object> liErrParams =
> lineItemError.getParameters();
>                                        error =
> MessageFormat.format(liErrTemplate, liErrParams.toArray());
>                                }
>                        }
>                }
>
>                return error;
>        }
>
>        @OnEvent(value=EventConstants.PREPARE_FOR_SUBMIT,
> component=ID_CART_FORM)
>        void beforeFormSubmit(int aIndex, int aLineNumber, String aSku) {
>                if(log.isDebugEnabled()) {
>                        log.debug("beforeFormSubmit -> aIndex: " + aIndex +
>                                        ", aLineNumber: " + aLineNumber + ",
> aSku: " + aSku);
>                }
>                index = aIndex;
>                submittedLineNumber = aLineNumber;
>                submittedSku = aSku;
>                indexedTrackers = new HashMap<Integer, ValidationTracker>();
>        }
>
>        @OnEvent(value=EventConstants.VALIDATE, component=ID_QUANTITY_FIELD)
>        void validateQuantity(int aQuantity) {
>
>                if(log.isDebugEnabled()) {
>                        log.debug("validating... aQuantity: " + aQuantity);
>                }
>
>                if(aQuantity <= 0) {
>                        String error =
>                                getMessages().format("error-quantity-value",
> submittedSku);
>                        cartForm.recordError(quantityField, error);
>                }
>        }
>
>        @OnEvent(value=EventConstants.FAILURE, component=ID_CART_FORM)
>        void failure() {
>                if(log.isDebugEnabled()) {
>                        log.debug("form submit ERROR!");
>                }
>        }
>
>        /**
>         * Performs update of quantity upon successful submission of a line
> item.
>         * Note that removal of the line item is not done as part of form
>         * submission, because we want to bypass any sort of line item
> validation
>         * if user simply wants to remove it.
>         */
>        @OnEvent(value=EventConstants.SUCCESS, component=ID_CART_FORM)
>        void onUpdate() {
>
>                if(log.isDebugEnabled()) {
>                        log.debug("form submit OK! lineNo: " +
> submittedLineNumber);
>                }
>
>                cartService.updateCart(
>                                getCartId(), submittedLineNumber,
> submittedQuantity);
>        }
>
>        /**
>         * Performs removal of a line item from a shopping cart, bypassing
> any
>         * form validation.
>         */
>        @OnEvent(value=EventConstants.ACTION, component=ID_REMOVE_LINK)
>        void onRemove(int aLineNumber) {
>                if(log.isDebugEnabled()) log.debug("remove lineNumber: " +
> aLineNumber);
>
>                cartService.removeFromCart(getCartId(), aLineNumber);
>        }
>
>        @Cached
>        public List<CartItemDisplayBean> getShoppingCartForDisplay() {
>
>                List<CartItemDisplayBean> cartItems = new
> LinkedList<CartItemDisplayBean>();
>                ShoppingCartBean shoppingCart = getShoppingCart();
>                List<CartItemBean> shoppingCartItems =
> shoppingCart.getCartItems();
>
>                for (CartItemBean ci : shoppingCartItems) {
>                        CartItemDisplayBean cartItem = new
> CartItemDisplayBean();
>                        ProductBean product =
> catalogService.getProduct(ci.getProductId(),
>                                        getLocale());
>                        // product may be null if invalid sku was added to
> the cart
>                        cartItem.setValid(product != null);
>                        if(cartItem.isValid()) {
>                                cartItem.setSku(product.getSku());
>                                cartItem.setPrice(ci.getCartPrice());
>                                cartItem.setQuantity(ci.getQuantity());
>                                String usuallyShipsMsg =
> buildUsuallyShipsMessage(ci
>                                                .getUsuallyShipsMessage());
>
>  cartItem.setUsuallyShipsMessage(usuallyShipsMsg);
>
>  cartItem.setDescription(product.getShortDescription());
>                                cartItem.setTotal(ci.getExtendedPrice());
>                                cartItem.setLineNumber(ci.getLineNumber());
>                        }
>                        else {
>                                cartItem.setSku(ci.getInvalidProductId());
>                                cartItem.setQuantity(ci.getQuantity());
>                                cartItem.setLineNumber(ci.getLineNumber());
>                        }
>                        cartItems.add(cartItem);
>                }
>
>                Collections.sort(cartItems);
>                return cartItems;
>        }
>
>        private String buildUsuallyShipsMessage(MessageHolderBean aBean) {
>                if(aBean == null) return "??";
>
>                String msgId = aBean.getMessageId();
>                List<Object> msgParams = aBean.getParameters();
>                String msgTemplate = getMessages().get(msgId);
>                String message = MessageFormat.format(msgTemplate,
> msgParams.toArray());
>
>                return message;
>        }
>
>        /**
>         * Given line number, locates line item within the shopping cart and
>         * returns its instance. If line number is invalid resulting in
> failed
>         * search, null is returned.
>         *
>         * @param aLineNumber line number for the item to be found
>         * @return
>         */
>        private CartItemBean findCartItem(int aLineNumber) {
>                ShoppingCartBean cart = getShoppingCart();
>                List<CartItemBean> cartItems = cart.getCartItems();
>                for(CartItemBean cartItem : cartItems) {
>                        int lineNumber = cartItem.getLineNumber();
>                        if(lineNumber == aLineNumber) return cartItem;
>                }
>                return null;
>        }
>
>        public int getQuantity() {
>                return cartDisplayItem.getQuantity();
>        }
>
>        public void setQuantity(int aQuantity) {
>                if(log.isTraceEnabled()) log.trace("setQuantity: " +
> aQuantity);
>                submittedQuantity = aQuantity;
>        }
>
>        public String getQuantityLabel() {
>                return submittedSku;
>        }
>
>        /**
>         * Determines if form under current loop iteration has errors or
> not.
>         *
>         * @return true if form has errors, false otherwise
>         */
>        public boolean isFormInError() {
>                ValidationTracker tracker = null;
>                if(indexedTrackers != null) {
>                        tracker = indexedTrackers.get(index);
>                }
>                return tracker != null && tracker.getHasErrors();
>        }
>
>        /**
>         * Determines if overall, entire shopping cart is valid. This
> includes
>         * persistent errors (line item errors), as well as UI input errors,
> all
>         * combined to a single boolean result. This value should be used to
>         * control the display of a generic state of the cart (or or not).
>         *
>         * @return true if cart is in perfect state, including all of its
> line
>         *      items, false if there is even the slightest problem.
>         */
>        public boolean isCartValid() {
>                ShoppingCartBean cart = getShoppingCart();
>                boolean valid = cart.isValid();
>                if(log.isDebugEnabled()) log.debug("cart.isValid ? " +
> valid);
>                if(valid && indexedTrackers != null) {
>                        if(log.isDebugEnabled()) log.debug("analyzing
> trackers...");
>                        Collection<ValidationTracker> trackers =
> indexedTrackers.values();
>                        for(ValidationTracker tracker : trackers) {
>                                valid = valid && !tracker.getHasErrors();
>                                if(!valid) break;
>                        }
>                }
>                return valid;
>        }
>
>        /**
>         * Determines if the cart line item under current loop iteration
> contains
>         * invalid sku. This information can be used to disable certain ui
>         * components such as quantity update button.
>         *
>         * @return true if cart item under current iteration contains
> invalid sku,
>         *      false otherwise
>         */
>        public boolean isLineItemWrongSku() {
>                boolean inError = false;
>                if(cartDisplayItem != null) inError =
> !cartDisplayItem.isValid();
>                return inError;
>         }
> }
>
> <t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
> t:title="${pageTitle}" xmlns:p="tapestry:parameter">
> <h1>${message:page-title}</h1>
> <style type="text/css">
> td.kk-currency-cell {
>  text-align: right;
> }
> </style>
>
> <div t:type="if" t:test="cartValid" style="float:right;font-weight:bold;">
>  <span style="color:green;">${message:cart-status-ok}</span>
>  <p:else><span
> style='color:red;'>${message:cart-status-error}</span></p:else>
> </div>
>
> <table border="1" width="100%" cellpadding="3">
> <tr>
> <th>${message:lineNumber-label}</th>
> <th>${message:sku-label}</th>
> <th>${message:description-label}</th>
> <th>${message:usuallyShipsMsg-label}</th>
> <th>${message:quantity-label}</th>
> <th>${message:price-label}</th>
> <th>${message:total-label}</th>
> <th width="140px"/>
> </tr>
> <t:loop t:source="shoppingCartForDisplay" t:value="cartDisplayItem"
> t:index="index">
> <t:form t:id="cartForm" t:context="[index, cartDisplayItem.lineNumber,
> cartDisplayItem.sku]">
>  <tr>
>  <td>${cartDisplayItem.lineNumber}</td>
>  <td><t:pagelink page="searchResults"
> context="cartDisplayItem.sku">${cartDisplayItem.sku}</t:pagelink></td>
>  <td>${cartDisplayItem.description}</td>
>  <td>${cartDisplayItem.usuallyShipsMessage}</td>
>  <td><t:textfield t:id="quantity" t:context="index"
> label="prop:quantityLabel" validate="required" size="2"/></td>
>  <td class='kk-currency-cell'><t:myt5lib.OutputLocale
> format="literal:currency" value="cartDisplayItem.price"/></td>
>  <td class='kk-currency-cell'><t:myt5lib.OutputLocale
> format="literal:currency" value="cartDisplayItem.total"/></td>
>  <td>
>   <input t:type="submit" t:id="update" t:disabled="lineItemWrongSku"
> t:value="message:update-value"/>
>  <a t:type="actionLink" t:id="remove"
> t:context="cartDisplayItem.lineNumber"><input type="button"
> value="${message:remove-value}"/></a>
>  </td>
>  </tr>
>  <t:if test="formInError">
>  <tr>
>  <td colspan="8"><t:error for="quantity"/></td>
>  </tr>
>  </t:if>
> </t:form>
> </t:loop>
> <tr>
>  <td colspan="4"></td>
>  <td>Subtotal:</td>
>  <td class='kk-currency-cell'><t:myt5lib.OutputLocale
> format="literal:currency" value="shoppingCart.cartSubTotal"/></td>
> </tr>
> <tr>
>  <td colspan="4"></td>
>  <td>Promo:</td>
>  <td class='kk-currency-cell'><t:myt5lib.OutputLocale
> format="literal:currency"
> value="shoppingCart.cartDiscountAmount"/></td>
> </tr>
> <tr>
>  <td colspan="4"></td>
>  <td>Total:</td>
>  <td class='kk-currency-cell'><t:myt5lib.OutputLocale
> format="literal:currency" value="shoppingCart.cartTotal"/></td>
> </tr>
> </table>
>
> </t:layout>
>
> Cheers! (I'm a happy guy today, the demo went well)
>
>  :-)  Adam
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: users-unsubscribe@tapestry.apache.org
> For additional commands, e-mail: users-help@tapestry.apache.org
>
>

Re: T5: form validation with pre-existing errors

Posted by Adam Zimowski <zi...@gmail.com>.
Tapestry friends!

Your silence combined with my milestone demo today only motivated me
to stay up couple of nights in a row, fire off debugger and step into
Tapestry source to get this solved. And so, amazing what can be done
under the pressure from management and when your job possibly may be
on the line...

In any case, my project is about 60% along the way, on target for go
live in Feb 2012. I now have a fully working advanced shopping cart. I
am sharing the code below for sake of completness of this thread, but
since my service layer runs the EJBs, the backend details are left
out. If there is interest, I can write a wiki page on advanced
shopping cart, covering the following features:

* pre existing errors (persisted by the ejb)
* form submission errors
* multiple form design (vs. single form)
* integrated use of Josh's (Canfield) AttachError mixin
* integrated use of indexed trackers

As a reminder, this shopping cart mimics functional behavior of our
current cart website: www.chdist.com , www.avenuesupply.ca and
www.industrialsupplies.com

Here is the code:

/**
 * Controls display and processing of data on the shopping cart page. Delegates
 * all business logic to the service layer.
 *
 * @author Adam Zimowski
 */
public class ShoppingCart extends BasePage {

	// component id's used within this class
	public static final String ID_CART_FORM = "cartForm";
	public static final String ID_QUANTITY_FIELD = "quantity";
	public static final String ID_REMOVE_LINK = "remove";
	
	@Inject
	private Logger log;
	
	@Inject
	private CatalogServiceRemote catalogService;

	@Inject
	private ShoppingCartServiceRemote cartService;

	@Property
	private CartItemDisplayBean cartDisplayItem;
	
	@Component(id=ID_QUANTITY_FIELD, parameters =
{"AttachError.message=fieldError"})
	@MixinClasses(value=AttachError.class)
	private TextField quantityField;
	
	@Component(parameters = {"tracker=tracker"})
	private Form cartForm;
	
	@Persist(PersistenceConstants.FLASH)
	private Map<Integer, ValidationTracker> indexedTrackers;

	@Property
	private int index;
	
	private int submittedQuantity;
	
	private int submittedLineNumber;
	
	private String submittedSku;

	
	public ValidationTracker getTracker() {
		if(indexedTrackers == null) return new ValidationTrackerImpl();
		return indexedTrackers.get(index);
	}
	
	public void setTracker(ValidationTracker aTracker) {
				
		if(indexedTrackers == null) {
			if(log.isTraceEnabled()) log.trace("crating indexed trackers map");
			indexedTrackers = new HashMap<Integer, ValidationTracker>();
		}
		
		if(log.isTraceEnabled()) {
			log.trace("setting tracker for index: " + index);
		}
		
		indexedTrackers.put(index, aTracker);
	}
	
	public String getFieldError() {
		String error = null;
		ValidationTracker tracker = getTracker();
		if(tracker != null && tracker.getError(quantityField) != null) {
			return null;
		}
		CartItemBean cib = findCartItem(cartDisplayItem.getLineNumber());
		if(!cib.isValid()) {
			List<MessageHolderBean> lineItemErrors = cib.getErrorMessages();
			int counter;
			for(counter = 0; counter < lineItemErrors.size(); ++counter) {
				MessageHolderBean lineItemError = lineItemErrors.get(counter);
				String liErrId = lineItemError.getMessageId();
				// for now we only pull out the first error; in the future we
				// may need to account for the entire collection. As of 4.0,
				// collection always contains only 1 item
				if(counter == 0) {
					String liErrTemplate = getMessages().get(liErrId);
					List<Object> liErrParams = lineItemError.getParameters();
					error = MessageFormat.format(liErrTemplate, liErrParams.toArray());
				}
			}
		}

		return error;
	}
	
	@OnEvent(value=EventConstants.PREPARE_FOR_SUBMIT, component=ID_CART_FORM)
	void beforeFormSubmit(int aIndex, int aLineNumber, String aSku) {
		if(log.isDebugEnabled()) {
			log.debug("beforeFormSubmit -> aIndex: " + aIndex +
					", aLineNumber: " + aLineNumber + ", aSku: " + aSku);
		}
		index = aIndex;
		submittedLineNumber = aLineNumber;
		submittedSku = aSku;
		indexedTrackers = new HashMap<Integer, ValidationTracker>();
	}

	@OnEvent(value=EventConstants.VALIDATE, component=ID_QUANTITY_FIELD)
	void validateQuantity(int aQuantity) {
		
		if(log.isDebugEnabled()) {
			log.debug("validating... aQuantity: " + aQuantity);
		}

		if(aQuantity <= 0) {
			String error =
				getMessages().format("error-quantity-value", submittedSku);
			cartForm.recordError(quantityField, error);
		}
	}
	
	@OnEvent(value=EventConstants.FAILURE, component=ID_CART_FORM)
	void failure() {
		if(log.isDebugEnabled()) {
			log.debug("form submit ERROR!");
		}
	}
	
	/**
	 * Performs update of quantity upon successful submission of a line item.
	 * Note that removal of the line item is not done as part of form
	 * submission, because we want to bypass any sort of line item validation
	 * if user simply wants to remove it.
	 */
	@OnEvent(value=EventConstants.SUCCESS, component=ID_CART_FORM)
	void onUpdate() {
		
		if(log.isDebugEnabled()) {
			log.debug("form submit OK! lineNo: " + submittedLineNumber);
		}
		
		cartService.updateCart(
				getCartId(), submittedLineNumber, submittedQuantity);
	}
	
	/**
	 * Performs removal of a line item from a shopping cart, bypassing any
	 * form validation.
	 */
	@OnEvent(value=EventConstants.ACTION, component=ID_REMOVE_LINK)
	void onRemove(int aLineNumber) {
		if(log.isDebugEnabled()) log.debug("remove lineNumber: " + aLineNumber);
		
		cartService.removeFromCart(getCartId(), aLineNumber);
	}

	@Cached
	public List<CartItemDisplayBean> getShoppingCartForDisplay() {

		List<CartItemDisplayBean> cartItems = new LinkedList<CartItemDisplayBean>();
		ShoppingCartBean shoppingCart = getShoppingCart();
		List<CartItemBean> shoppingCartItems = shoppingCart.getCartItems();

		for (CartItemBean ci : shoppingCartItems) {
			CartItemDisplayBean cartItem = new CartItemDisplayBean();
			ProductBean product = catalogService.getProduct(ci.getProductId(),
					getLocale());
			// product may be null if invalid sku was added to the cart
			cartItem.setValid(product != null);
			if(cartItem.isValid()) {
				cartItem.setSku(product.getSku());
				cartItem.setPrice(ci.getCartPrice());
				cartItem.setQuantity(ci.getQuantity());
				String usuallyShipsMsg = buildUsuallyShipsMessage(ci
						.getUsuallyShipsMessage());
				cartItem.setUsuallyShipsMessage(usuallyShipsMsg);
				cartItem.setDescription(product.getShortDescription());
				cartItem.setTotal(ci.getExtendedPrice());
				cartItem.setLineNumber(ci.getLineNumber());
			}
			else {
				cartItem.setSku(ci.getInvalidProductId());
				cartItem.setQuantity(ci.getQuantity());
				cartItem.setLineNumber(ci.getLineNumber());
			}
			cartItems.add(cartItem);
		}

		Collections.sort(cartItems);
		return cartItems;
	}

	private String buildUsuallyShipsMessage(MessageHolderBean aBean) {
		if(aBean == null) return "??";
		
		String msgId = aBean.getMessageId();
		List<Object> msgParams = aBean.getParameters();
		String msgTemplate = getMessages().get(msgId);
		String message = MessageFormat.format(msgTemplate, msgParams.toArray());
		
		return message;
	}
	
	/**
	 * Given line number, locates line item within the shopping cart and
	 * returns its instance. If line number is invalid resulting in failed
	 * search, null is returned.
	 *
	 * @param aLineNumber line number for the item to be found
	 * @return
	 */
	private CartItemBean findCartItem(int aLineNumber) {
		ShoppingCartBean cart = getShoppingCart();
		List<CartItemBean> cartItems = cart.getCartItems();
		for(CartItemBean cartItem : cartItems) {
			int lineNumber = cartItem.getLineNumber();
			if(lineNumber == aLineNumber) return cartItem;
		}
		return null;
	}
	
	public int getQuantity() {
		return cartDisplayItem.getQuantity();
	}
	
	public void setQuantity(int aQuantity) {
		if(log.isTraceEnabled()) log.trace("setQuantity: " + aQuantity);
		submittedQuantity = aQuantity;
	}
	
	public String getQuantityLabel() {
		return submittedSku;
	}
	
	/**
	 * Determines if form under current loop iteration has errors or not.
	 *
	 * @return true if form has errors, false otherwise
	 */
	public boolean isFormInError() {
		ValidationTracker tracker = null;
		if(indexedTrackers != null) {
			tracker = indexedTrackers.get(index);
		}
		return tracker != null && tracker.getHasErrors();
	}
	
	/**
	 * Determines if overall, entire shopping cart is valid. This includes
	 * persistent errors (line item errors), as well as UI input errors, all
	 * combined to a single boolean result. This value should be used to
	 * control the display of a generic state of the cart (or or not).
	 *
	 * @return true if cart is in perfect state, including all of its line
	 * 	items, false if there is even the slightest problem.
	 */
	public boolean isCartValid() {
		ShoppingCartBean cart = getShoppingCart();
		boolean valid = cart.isValid();
		if(log.isDebugEnabled()) log.debug("cart.isValid ? " + valid);
		if(valid && indexedTrackers != null) {
			if(log.isDebugEnabled()) log.debug("analyzing trackers...");
			Collection<ValidationTracker> trackers = indexedTrackers.values();
			for(ValidationTracker tracker : trackers) {
				valid = valid && !tracker.getHasErrors();
				if(!valid) break;
			}
		}
		return valid;
	}
	
	/**
	 * Determines if the cart line item under current loop iteration contains
	 * invalid sku. This information can be used to disable certain ui
	 * components such as quantity update button.
	 *
	 * @return true if cart item under current iteration contains invalid sku,
	 * 	false otherwise
	 */
	public boolean isLineItemWrongSku() {
		boolean inError = false;
		if(cartDisplayItem != null) inError = !cartDisplayItem.isValid();
		return inError;
	}
}

<t:layout xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
t:title="${pageTitle}" xmlns:p="tapestry:parameter">
<h1>${message:page-title}</h1>
<style type="text/css">
td.kk-currency-cell {
 text-align: right;
}
</style>

<div t:type="if" t:test="cartValid" style="float:right;font-weight:bold;">
 <span style="color:green;">${message:cart-status-ok}</span>
 <p:else><span style='color:red;'>${message:cart-status-error}</span></p:else>
</div>

<table border="1" width="100%" cellpadding="3">
<tr>
<th>${message:lineNumber-label}</th>
<th>${message:sku-label}</th>
<th>${message:description-label}</th>
<th>${message:usuallyShipsMsg-label}</th>
<th>${message:quantity-label}</th>
<th>${message:price-label}</th>
<th>${message:total-label}</th>
<th width="140px"/>
</tr>
<t:loop t:source="shoppingCartForDisplay" t:value="cartDisplayItem"
t:index="index">
<t:form t:id="cartForm" t:context="[index, cartDisplayItem.lineNumber,
cartDisplayItem.sku]">
 <tr>
 <td>${cartDisplayItem.lineNumber}</td>
 <td><t:pagelink page="searchResults"
context="cartDisplayItem.sku">${cartDisplayItem.sku}</t:pagelink></td>
 <td>${cartDisplayItem.description}</td>
 <td>${cartDisplayItem.usuallyShipsMessage}</td>
 <td><t:textfield t:id="quantity" t:context="index"
label="prop:quantityLabel" validate="required" size="2"/></td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency" value="cartDisplayItem.price"/></td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency" value="cartDisplayItem.total"/></td>
 <td>
  <input t:type="submit" t:id="update" t:disabled="lineItemWrongSku"
t:value="message:update-value"/>
  <a t:type="actionLink" t:id="remove"
t:context="cartDisplayItem.lineNumber"><input type="button"
value="${message:remove-value}"/></a>
 </td>
 </tr>
 <t:if test="formInError">
 <tr>
  <td colspan="8"><t:error for="quantity"/></td>
 </tr>
 </t:if>
</t:form>
</t:loop>
<tr>
 <td colspan="4"></td>
 <td>Subtotal:</td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency" value="shoppingCart.cartSubTotal"/></td>
</tr>
<tr>
 <td colspan="4"></td>
 <td>Promo:</td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency"
value="shoppingCart.cartDiscountAmount"/></td>
</tr>
<tr>
 <td colspan="4"></td>
 <td>Total:</td>
 <td class='kk-currency-cell'><t:myt5lib.OutputLocale
format="literal:currency" value="shoppingCart.cartTotal"/></td>
</tr>
</table>

</t:layout>

Cheers! (I'm a happy guy today, the demo went well)

 :-)  Adam

---------------------------------------------------------------------
To unsubscribe, e-mail: users-unsubscribe@tapestry.apache.org
For additional commands, e-mail: users-help@tapestry.apache.org