Select update via zone inside a form confuses form's ValidationTracker
----------------------------------------------------------------------
Key: TAP5-1512
URL: https://issues.apache.org/jira/browse/TAP5-1512
Project: Tapestry 5
Issue Type: Bug
Components: tapestry-core, tapestry-ioc
Affects Versions: 5.2.5
Reporter: Adam Zimowski
Priority: Minor
Here is the use case:
We have a simple address form with Line 1, Line 2, City (all text fields),
State (Select), Zip (Text Field) and Country (Select). State select is wrapped
inside a Zone, which is tied to Country select, so that when country is
selected, states get populated. Line 1, City, State, Zip and Country are all
required. Form uses <t:error/> next to each field.
Here is the problem:
1. Select country and state (both have blank options therefore are required),
but leave other fields empty.
2. Submit form. Validation on required fields (such as city, zip) results in
form rendering error messages. Note: Country and State are
not in error at this point.
3. While correcting errors on the form, change country. As a result, state
select component is repopulated (zone update), and default blank
option is select. Do not chose state.
4. Submit form with state *not* selected. Debugging select shows that
ValidationTracker contains error (state required), yet attached field error is
not displaying the error.
Upon debugging the framework I found the following:
IdAllocator used to generate component id, suffixes select with an increment
even select is not inside a loop. It does this ONLY when Zone is updated,
causing the very subsequent form submit record error for select under wrong key
(a_state_1 rather than state), and so the <t:error/> is unable to locate the
error when form eventually renders back with original select id (a_state).
In this case, what happens is that every time I update zone while form is in
error, the ID of my select changes with incremented suffix:
ORIGINAL AS EXPECTED BY FORM: a_state
After resetting country causing state dropdown to repopulate, id becomes:
a_state_1 then a_state_2 etc...
This causes ValidationTracker put error under wrong key, and consequently error
to field binding in tracker's map cannot be
resolved, causing <t:error/> thinking that all is good. That's why <t:errors/>
does display the error, as it simply loops over collection
of errors.
Out of lack of deep understanding of Tapestry, I coded a simple hack to verify
that if select update via zone kept its original id things
would work, and indeed, the following hack fixes the problem for my case:
Tapestry 5.2.5, AbstractField line 183:
private void setupControlName(String controlName)
{
if(controlName.startsWith("a_state"))
this.controlName = "a_state";
else
this.controlName = controlName;
}
Here is my source:
<t:form t:id="registrationForm">
<div class="kk-hdr">Address Information</div>
<div class="kk-row">
<div class="kk-label"><t:label for="a_line1"/> :</div>
<div class="kk-field"><t:textfield t:id="a_line1" value="address.line1"/></div>
<t:error class="literal:kk-error" for="a_line1"/>
</div>
<div class="kk-row">
<div class="kk-label"><t:label for="a_line2"/> :</div>
<div class="kk-field"><t:textfield t:id="a_line2" value="address.line2"/></div>
<t:error class="literal:kk-error" for="a_line2"/>
</div>
<div class="kk-row">
<div class="kk-label"><t:label for="a_line3"/> :</div>
<div class="kk-field"><t:textfield t:id="a_line3" value="address.line3"/></div>
<t:error class="literal:kk-error" for="a_line3"/>
</div>
<div class="kk-row">
<div class="kk-label"><t:label for="a_city"/> :</div>
<div class="kk-field"><t:textfield t:id="a_city" value="address.city"/></div>
<t:error class="literal:kk-error" for="a_city"/>
</div>
<div class="kk-row">
<div class="kk-label"><t:label for="a_zip"/> :</div>
<div class="kk-field"><t:textfield t:id="a_zip" value="address.zipCode"/></div>
<t:error class="literal:kk-error" for="a_zip"/>
</div>
<div class="kk-row">
<div class="kk-label"><t:label for="a_state"/> :</div>
<div class="kk-field" t:type="zone" t:id="stateModelZone"><t:select
t:id="a_state" model="stateModel" validate="required"
value="address.stateCode" blankOption="ALWAYS"
blankLabel="literal:--Please Select"/></div>
<t:error class="literal:kk-error" for="a_state"/>
</div>
<div class="kk-row">
<div class="kk-label"><t:label for="a_country"/> :</div>
<div class="kk-field"><t:select t:id="a_country" model="countryModel"
value="address.countryCode" blankOption="NEVER"
zone="stateModelZone"/></div>
</div>
<p>
<input t:type="submit" value="message:submit-label"/>
</p>
</t:form>
public class Register extends BasePage {
@Inject
private Logger log;
@Inject
private UtilityServiceRemote utilityService;
@Persist
@Property
private AddressUiBean address;
@OnEvent(value=EventConstants.PREPARE)
void initialize() {
if(address == null) address = new AddressUiBean();
if(contact == null) contact = new ContactUiBean();
if(registration == null) registration = new RegisterUiBean();
String countryCode = address.getCountryCode();
if(countryCode == null) {
Locale locale = getLocale();
countryCode = locale.getCountry();
address.setCountryCode(countryCode);
}
log.debug("address state code {}", address.getStateCode());
}
@Cached
public Map<String, String> getCountryModel() {
Map<String, String> model = new LinkedHashMap<String, String>();
List<CountryBean> countries =
utilityService.getAllCountries(getLocale());
for(CountryBean country : countries) {
String code = country.getCodeIsoAlpha2();
String description = country.getShortName();
log.debug("code: {}, description: {}", code,
description);
model.put(code, description);
}
return model;
}
@OnEvent(value=EventConstants.VALUE_CHANGED, component="a_country")
public Object onCountrySelected(String aCountryCode) {
log.debug("selected country: {}", aCountryCode);
address.setStateCode(null);
return stateModelZone.getBody();
}
@Cached
public Map<String, String> getStateModel() {
Map<String, String> model = new LinkedHashMap<String, String>();
String countryCode = address.getCountryCode();
List<StateProvinceBean> states =
utilityService.getAllStateProvincesForCountry(countryCode, getLocale());
for(StateProvinceBean state : states) {
String code = state.getLookupCode();
String name = state.getLongName();
log.debug("code: {}, name {}", code, name);
model.put(code, name);
}
return model;
}
}
--
This message is automatically generated by JIRA.
For more information on JIRA, see: http://www.atlassian.com/software/jira