Checklist
component:
Checklist.tml,
Checklist.java.
<!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 As a Field (1)</h3>
The sub-form in this example does its own validation AND it links to its label because it extends <em>AbstractField</em>
and renders a div with the field's id, so when there's an error in the sub-form the label will be decorated.
Try Save with no people selected.<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">
<t:label for="invitedPersons" class="col-sm-3"/>
<div class="col-sm-3">
<t:examples.component.SelectPersonsField t:id="invitedPersons" class="form-control" 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>
This solution does not handle Tapestry and JSR-303 validators. For a comprehensive solution see Tapestry's <code>Checklist</code> component:
<a href="http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/corelib/components/Checklist.tml?view=markup">Checklist.tml</a>,
<a href="http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/java/org/apache/tapestry5/corelib/components/Checklist.java?view=markup">Checklist.java</a>.
<br/><br/>
References:
<a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/corelib/base/AbstractField.html">AbstractField</a>,
<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/><br/>
<t:tabgroup>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/component/SubFormAsAField1.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/component/SubFormAsAField1.properties"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/component/SubFormAsAField1.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/SelectPersonsField.tml"/>
<t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/examples/component/SelectPersonsField.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 SubFormAsAField1 {
static private final int MAX_RESULTS = 30;
// Screen fields
@Property
// @SessionState(create = false)
private Invitation invitation;
@Property
private List<Person> allPersons;
// Other pages
@InjectPage
private SubFormAsAField2 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;
}
if (invitation.getEventDescription().startsWith("a")) {
form.recordError("Event desc must not start with \"a\".");
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 id="${clientId}" 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.EventConstants;
import org.apache.tapestry5.ValidationTracker;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.corelib.base.AbstractField;
import org.apache.tapestry5.services.FormSupport;
@Events(EventConstants.VALIDATE)
public class SelectPersonsField extends AbstractField {
// 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;
// Screen fields
@Property
private Person person;
// Generally useful bits and pieces
@Environmental
private FormSupport formSupport;
@Environmental
private ValidationTracker tracker;
private static final ProcessSubmissionAfter PROCESS_SUBMISSION_AFTER = new ProcessSubmissionAfter();
// 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_AFTER in its list of actions to do on
// submit. If I contain other components (which I do), their actions will already be in the list, before
// PROCESS_SUBMISSION_AFTER. That is because this method, afterRender(), is late in the sequence. This
// guarantees PROCESS_SUBMISSION_AFTER will be executed on submit AFTER the components I contain are processed
// (which includes their validation).
if (formSupport != null) {
formSupport.store(this, PROCESS_SUBMISSION_AFTER);
}
}
@Override
protected void processSubmission(final String controlName) {
// Nothing to do yet, because it's before my components are handled.
}
private static class ProcessSubmissionAfter implements ComponentAction<SelectPersonsField> {
private static final long serialVersionUID = 1L;
public void execute(SelectPersonsField component) {
component.processSubmissionAfter();
}
@Override
public String toString() {
return this.getClass().getSimpleName() + ".ProcessSubmissionAfter";
}
};
protected void processSubmissionAfter() {
// Error if the number of persons chosen is less than specified by the min parameter.
if (chosenPersons.size() < min) {
tracker.recordError(this, "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);
}
}
@Override
public boolean isRequired() {
return true;
}
}
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;
}
}