we are encountering an issue with Tapestry (version 5.7.2) and zone refreshing from components.
We have a page that contains a loop of (zoned) components, where each component has a async event (see code).
What we want to achieve, is that via the XHR refresh, we refresh both the component zone and another zone that is contained by the page, which we get via an interface.
In this code example, when we click on the first component, it refreshes the zone but 'forgets' the @Persist annotated field, which takes the value of the second component.
If we click on the second component instead, it refreshes the first one too.
What are we doing wrong? seems trivial zone refreshing but we didn't get it, we tried different approaches but must fallback to handle this part in a less elegant way.
Page code:
@InjectComponent
private Zone listingZone;
@Persist
private String[] names;
@Property
private String lastRefreshZoneName;
@Property
private String _name;
Object onActivate() throws Exception {
names = new String[]{"first","second"}; //eg. loaded from DB
return null;
}
public LocalDateTime getTime() {
return LocalDateTime.now();
}
public String[] getNames() {
return names;
}
@Override
public Zone getOuterZone() {
return listingZone;
}
@Override
public void onTriggerOn(String name) {
lastRefreshZoneName = name;
}
with a simple TML as such:
<t:zone t:id="pageZone">
Page zone: ${time}<br/>
<hr/>
<t:loop source="names" value="name">
<t:attribute.zonedcomponent t:parameter="${name}"/>
</t:loop>
<hr/>
<t:zone t:id="listingZone">
Listing zone, last refresh: ${time}<br/>
Last refresh zone name: ${lastRefreshZoneName}
</t:zone>
</t:zone>
for this example, ZonedComponent is a component with a zone and a event link as such:
@Inject
private Request request;
@Inject
private AjaxResponseRenderer ajaxResponseRenderer;
@Inject
private ComponentResources resources;
@InjectComponent
private Zone componentZone;
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private String parameter;
@Persist
@Property
private String name;
void setupRender() {
this.name = parameter;
}
public LocalDateTime getTime() {
return LocalDateTime.now();
}
void onTrigger(String name) {
if (request.isXHR()) {
// this.name = name; // if we don't uncomment this, then it doesnt even propagate the 'name' correctly
SomeInterface page = (SomeInterface)resources.getPage();
page.onTriggerOn(this.name);
ajaxResponseRenderer.addRender(componentZone)
.addRender(page.getOuterZone());
}
}
public static interface SomeInterface {
ClientBodyElement getOuterZone();
void onTriggerOn(String name);
}
with the tml zone as such:
<t:zone t:id="componentZone" style="border:1px solid black">
Component name: ${name}<br/>
Component zone: ${time}<br/>
<t:eventlink t:event="trigger" t:context="${name}" async="true">
async event from ${name}
</t:eventlink>
</t:zone>
There are three key points to learn from your example.
Component (server-side) Ids vs. Client Ids - When working with Ajax and Zones, the component event handlers need to know the client-side element id. You can simply hard-code one in the template file. However, when using the component within a loop, the id is no longer unique and the event handler has no way of knowing which client-side element (not) to update. One solution is to use the
JavaScriptSupportservice to allocate a client id.Event bubbling - Components events don't have do be handled within the component they were triggered. They can actually 'bubble up' from nested components to outer components/pages. It is also possible to handle the event in both places. This allows you to simplify your code quite a bit: no need to get the containing component/page an invoke a method of an interface you had to introduce to make it work. See the Event Bubbling section on the Components Events page in the Tapestry docs for more details.
Component parameters - Component parameters have a Java type. When passing parameter values, simply refer to a property. Use the
${...}syntax only where you need an expression converted to string. See section 'Don't use the ${...} syntax!' on the Component Parameters page in the Tapestry docs.Having learned the above your example can be rewritten as follows.
The page class:
The page template:
The component class:
The component template: