<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like
We use a "strict" DTD to make IE follow the alignment rules. -->
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<body class="container">
<h3>Sub-Form Validation (1)</h3>
The sub-form in this example does its own validation, with the help of Tapestry's FormSupport and ValidationTracker.<br/><br/>
FormSupport holds a list of actions for the Form to execute on submit. Our sub-form adds its processSubmission() to that list.<br/>
ValidationTracker lets us record error messages.<br/>
<div class="eg">
<t:form t:id="form" validate="invitation" class="form-horizontal well">
<h4>Create Invitation</h4>
<div class="form-group">
<t:label for="eventDescription" class="col-sm-3"/>
<div class="col-sm-3">
<t:textfield t:id="eventDescription" value="invitation.eventDescription"/>
</div>
</div>
<div class="form-group">
<label for="invitedPersons" class="col-sm-3 control-label">${message:invitedPersons-label}</label>
<div class="col-sm-3">
<t:examples.component.SelectPersonsValidated t:id="invitedPersons" persons="allPersons"
chosen="invitation.invitedPersons" min="1" />
</div>
</div>
<div class="form-group">
<div class="col-sm-3 col-sm-offset-3">
<t:submit value="Save"/>
</div>
</div>
<t:errors globalOnly="true"/>
</t:form>
</div>
References:
<a href="http://tapestry.apache.org/environmental-services.html">Environmental Services</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/FormSupport.html">FormSupport</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ValidationTracker.html">ValidationTracker</a>,
<a href="http://tapestry.apache.org/forms-and-validation.html">Forms and Validation</a>,
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/ComponentAction.html">ComponentAction</a>,
<a href="http://getbootstrap.com/components/#wells">.well</a>.<br/><br/>
<t:pagelink page="Index">Home</t:pagelink><br/><br/>
The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.<br/>
<t:tabgroup>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/component/SubFormValidation1.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/component/SubFormValidation1.properties"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/component/SubFormValidation1.java"/>
<t:sourcecodetab src="/web/src/main/resources/META-INF/assets/css/examples/subform.css"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/examples/component/SelectPersonsValidated.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/examples/component/SelectPersonsValidated.java"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/models/examples/Invitation.java"/>
</t:tabgroup>
</body>
</html>
invitedPersons-label=People to Invite
package jumpstart.web.pages.examples.component;
import java.util.List;
import javax.ejb.EJB;
import jumpstart.business.domain.person.Person;
import jumpstart.business.domain.person.iface.IPersonFinderServiceLocal;
import jumpstart.util.ExceptionUtil;
import jumpstart.web.models.examples.Invitation;
import org.apache.tapestry5.annotations.Import;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.InjectPage;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.components.Form;
@Import(stylesheet = "css/examples/subform.css")
public class SubFormValidation1 {
static private final int MAX_RESULTS = 30;
// Screen fields
@Property
private Invitation invitation;
@Property
private List<Person> allPersons;
// Other pages
@InjectPage
private SubFormValidation2 page2;
// Generally useful bits and pieces.
@InjectComponent
private Form form;
@EJB
private IPersonFinderServiceLocal personFinderService;
// The code
// Form bubbles up the PREPARE event during form render and form submission.
void onPrepare() {
invitation = new Invitation();
allPersons = personFinderService.findPersons(MAX_RESULTS);
}
void onValidateFromForm() {
if (form.getHasErrors()) {
// We get here only if a server-side validator detected an error.
return;
}
// Create the invitation
try {
// In a real application we would persist the invitation to the database
// personManagerService.createInvitation(invitation.eventDescription, invitation.invitedPersons);
}
catch (Exception e) {
// Display the cause. In a real system we would try harder to get a user-friendly message.
form.recordError(ExceptionUtil.getRootCauseMessage(e));
}
}
Object onSuccess() {
// In a real application we would pass the invitation id instead of the invitation.
page2.set(invitation);
return page2;
}
}
.eg {
margin: 20px 0;
padding: 14px;
border: 1px solid #ddd;
border-radius: 6px;
-webkit-border-radius: 6px;
-mox-border-radius: 6px;
}
.eg .person-row {
height: 24px;
}
.eg .person-row label {
margin-left: 6px;
font-weight: normal;
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like
We use a "strict" DTD to make IE follow the alignment rules. -->
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<t:content>
<div class="form-group well">
<t:loop source="persons" value="person" formstate="iteration">
<div class="form-inline person-row">
<t:checkbox t:id="person" value="personChosen" disabled="prop:disabled"/>
<t:label for="person">${person.firstName} ${person.lastName}</t:label>
</div>
</t:loop>
</div>
</t:content>
</html>
package jumpstart.web.components.examples.component;
import java.util.Set;
import jumpstart.business.domain.person.Person;
import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ComponentAction;
import org.apache.tapestry5.ValidationTracker;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.services.FormSupport;
public class SelectPersonsValidated {
// Parameters
@Parameter(required = true, allowNull = false)
@Property
private Iterable<Person> persons;
@Parameter(required = true, allowNull = false, name = "chosen")
@Property
private Set<Person> chosenPersons;
@Parameter(defaultPrefix = BindingConstants.LITERAL, value = "0")
@Property
private int min;
@Parameter(defaultPrefix = BindingConstants.LITERAL, value = "false")
@Property
private boolean disabled;
// Screen fields
@Property
private Person person;
// Generally useful bits and pieces
@Environmental
private FormSupport formSupport;
@Environmental
private ValidationTracker tracker;
private static final ProcessSubmission PROCESS_SUBMISSION = new ProcessSubmission();
// The code
// Tapestry calls afterRender() AFTER it renders any components I contain (ie. Loop).
final void afterRender() {
// If we are inside a form, ask FormSupport to store PROCESS_SUBMISSION in its list of actions to do on submit.
// If I contain other components, their actions will already be in the list, before PROCESS_SUBMISSION. That is
// because this method, afterRender(), is late in the sequence. This guarantees PROCESS_SUBMISSION will be
// executed on submit AFTER the components I contain are processed (which includes their validation).
if (formSupport != null) {
formSupport.store(this, PROCESS_SUBMISSION);
}
}
private static class ProcessSubmission implements ComponentAction<SelectPersonsValidated> {
private static final long serialVersionUID = 1L;
public void execute(SelectPersonsValidated component) {
component.processSubmission();
}
@Override
public String toString() {
return this.getClass().getSimpleName() + ".ProcessSubmission";
}
};
private void processSubmission() {
// Validate. We ensured in afterRender() that the components I contain have already been validated.
// Error if the number of persons chosen is less than specified by the min parameter.
if (chosenPersons.size() < min) {
tracker.recordError("You must choose at least " + min + " person(s).");
return;
}
}
// The Loop component will automatically call this for every row as it is rendered.
public boolean isPersonChosen() {
return chosenPersons.contains(person);
}
// The Loop component will automatically call this for every row on submit.
public void setPersonChosen(boolean personChosen) {
if (personChosen) {
chosenPersons.add(person);
}
else {
chosenPersons.remove(person);
}
}
}
package jumpstart.web.models.examples;
import java.util.HashSet;
import java.util.Set;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import jumpstart.business.domain.person.Person;
public class Invitation {
@NotNull
@Size(max = 50)
private String eventDescription;
private Set<Person> invitedPersons;
public String toString() {
final String DIVIDER = ", ";
StringBuilder buf = new StringBuilder();
buf.append(this.getClass().getSimpleName() + ": ");
buf.append("[");
buf.append("eventDescription=" + eventDescription + DIVIDER);
buf.append("invitedPersons=" + invitedPersons);
buf.append("]");
return buf.toString();
}
public Invitation() {
eventDescription = null;
invitedPersons = new HashSet<Person>();
}
public String getEventDescription() {
return eventDescription;
}
public void setEventDescription(String eventDescription) {
this.eventDescription = eventDescription;
}
public Set<Person> getInvitedPersons() {
return invitedPersons;
}
public void setInvitedPersons(Set<Person> invitedPersons) {
this.invitedPersons = invitedPersons;
}
}