Basic looping class; loops over a number of items (provided by its source parameter), rendering its body for each one. It turns out that gettting the component to not store its state in the Form is very tricky and, in fact, a series of commands for starting and ending heartbeats, and advancing through the iterator, are still stored. For a non-volatile Loop inside the form, the Loop stores a series of commands that start and end heartbeats and store state (either as full objects when there the encoder parameter is not bound, or as client-side objects when there is an encoder). For a Loop that doesn't need to be aware of the enclosing Form (if any), the formState parameter should be bound to 'none'. When the Loop is used inside a Form, it will generate an EventConstants#SYNCHRONIZE_VALUES event to inform its container what values were submitted and in what order.
| Name | Type | Flags | Default | Default Prefix | Since | Description |
|---|---|---|---|---|---|---|
| element | String | NOT Allow Null | literal | The element to render. If not null, then the loop will render the indicated element around its body (on each pass through the loop). The default is derived from the component template. | ||
| empty | Block | NOT Allow Null | literal | A Block to render instead of the loop when the source is empty. The default is to render nothing. | ||
| encoder | ValueEncoder | NOT Allow Null | prop | Optional value converter; if provided (or defaulted) and inside a form and not volatile, then each iterated value is converted and stored into the form. A default for this is calculated from the type of the property bound to the value parameter. | ||
| formState | LoopFormState | NOT Allow Null | literal | Controls what information, if any, is encoded into an enclosing Form. The default value for this is set by the deprecated volatile parameter. The normal default is LoopFormState#VALUES, but changes to LoopFormState#ITERATION if volatile is true. This parameter is only used if the component is enclosed by a Form. | ||
| index | int | NOT Allow Null | prop | The index into the source items. | ||
| source | Iterable | Required, NOT Allow Null | prop | Defines the collection of values for the loop to iterate over. If not specified, defaults to a property of the container whose name matches the Loop cmponent's id. | ||
| value | Object | NOT Allow Null | prop | The current value, set before the component renders its body. | ||
| volatile | boolean | NOT Allow Null | prop | If true and the Loop is enclosed by a Form, then the normal state saving logic is turned off. Defaults to false, enabling state saving logic within Forms. With the addition of the formState parameter, volatile simply sets a default for formState is formState is not specified. |
Informal parameters: supported
<table class="navigation" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<tr>
<t:loop source="pageNames" value="pageName">
<td class="${tabClass}">
<t:pagelink page="pageName">${pageName}</t:pagelink>
</td>
</t:loop>
</tr>
</table>We are assuming that the NavBar component has a pageNames property (possibly a parameter). The Loop will iterate over those page names and store each into its value parameter.
public class NavBar
{
@Parameter(defaultPrefix="literal", required=true)
private String pages;
@Inject
private ComponentResources resources;
@Property
private String _pageName;
public String[] getPageNames()
{
return pages.split(",");
}
public String getTabClass()
{
if (pageName.equalsIgnoreCase(resources.getPageName())
return "current";
return null;
}
}
The component converts its pages parameter into the pageNames property by splitting it at the commas. It tracks the current pageName of the loop not just to generate the links, but to calculate the CSS class of each <td> element on the fly. This way we can give the tab corresponding to the current page a special look or highlight.
We can fold together the Loop component and the <td> element:
<table class="navigation" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<tr>
<td t:type="loop" source="pageNames" value="pageName" class="${tabClass}">
<t:pagelink page="pageName">${pageName}</t:pagelink>
</td>
</tr>
</table>Using the
t:type="loop"
attribute, the other way to identify a template
element as a component, allows the Loop component to render the element's tag,
the <td> on each iteration, along with informal parameters (the class attribute). This is
calledinvisible instrumentation, and it is more concise and more
editor/preview friendly than Tapestry's typical markup.
Tapestry form control element components (TextField, etc.) work inside loops. However, some additional configuration is needed to make this work efficiently.
With no extra configuration, each value object will be serialized into the form (if you view the rendered markup, you'll see a hidden form field containing serialized data needed by Tapestry to process the form). This can become very bloated, or may not work if the objects being iterated are not serializable.
The typical case is database driven; you are editting objects from a database and need those objects back when the form is submitted. All that should be stored on the client is the ids of those objects. Thats what the encoder parameter is for.
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<body>
<h1>Edit Order Quantities</h1>
<t:form>
<t:errors/>
<t:loop source="items" value="item" encoder="encoder">
<div class="line-item">
<t:label for="quantity">${item.product.name}</t:label>
<t:textfield t:id="quantity" value="item.quantity"/>
</div>
</t:loop>
<input type="submit" value="Update"/>
</t:form>
</body>
</html>The TextField component is rendered multiple times, once for each LineItem in the Order.
public class EditOrder
{
@Inject
private OrderDAO orderDAO;
@Property
private final PrimaryKeyEncoder<Long,LineItem> encoder = new PrimaryKeyEncoder<Long,LineItem>()
{
public Long toKey(LineItem value) { return value.getId(); }
public void prepareForKeys(List<Long> keys) { }
public LineItem toValue(Long key)
{
return orderDAO.getLineItem(key);
}
};
@Persist
private long orderId;
@Property
private LineItem item;
public List<LineItem> getItems()
{
return orderDAO.getLineItemsForOrder(orderId);
}
}Here, we expect the OrderDAO service to do most of the work, and we create a wrapper around it, in the form of the PrimeryKeyEncoder instance.
We've glossed over a few issues here, including how to handle the case that a particular item has been deleted or changed between the render request and the form submission, as well as how the orderId property gets set in the first place.
Accounting for those situations would largely be encapsulated inside the PrimeryKeyEncoder instance.