Sub-Form Validation (1)

The sub-form in this example does its own validation, with the help of Tapestry's FormSupport and ValidationTracker.

FormSupport holds a list of actions for the Form to execute on submit. Our sub-form adds its processSubmission() to that list.
ValidationTracker lets us record error messages.

Create Invitation

References: Environmental Services, FormSupport, ValidationTracker, Forms and Validation, ComponentAction, .well.

Home

The source for IPersonFinderServiceLocal and @EJB is shown in the Session Beans and @EJB examples.

<!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 &nbsp; 
     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 &nbsp; 
     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}&nbsp;${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;
    }

}