Sub-Form Component (1)

We will use an invented term, "sub-form", to mean a component that contains input components. It will be put in a Form.
In this example we have a sub-form called SelectPersons which the page uses to edit invitation.invitedPersons.
On submit, the page validates invitation.invitedPersons.

Create Invitation

We would like the sub-form to do some validation, but where? There is no point putting validation in an event handler method like onValidateFromForm() because the method will never be triggered. Yes, Form will trigger event VALIDATE, but it wil bubble up to its container, which is this page, not down into its body, which is where this sub-form sits. We solve this in the next two examples.

References: Forms and Validation, .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 Component (1)</h3>
    
    We will use an invented term, "sub-form", to mean a component that contains input components. It will be put in a Form.<br/>
    In this example we have a sub-form called SelectPersons which the page uses to edit <em>invitation.invitedPersons</em>.<br/>
    On submit, the page validates <em>invitation.invitedPersons</em>.<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-4" />
                <div class="col-sm-4">
                    <t:textfield t:id="eventDescription" value="invitation.eventDescription"/>
                </div>
            </div>
            <div class="form-group">
                <label for="invitedPersons" class="col-sm-4 control-label">${message:invitedPersons-label}</label>
                <div class="col-sm-4">
                    <t:examples.component.SelectPersons t:id="invitedPersons" persons="allPersons" chosen="invitation.invitedPersons" />
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-4 col-sm-offset-4">
                    <t:submit value="Save"/>
                </div>
            </div>

            <t:errors globalOnly="true"/>
        </t:form>
        
    </div>

    We would like the sub-form to do some <strong>validation</strong>, but where? 
    There is no point putting validation in an event handler method like <em>onValidateFromForm()</em> because the 
    method will never be triggered. Yes, Form will trigger event VALIDATE, but it wil bubble up to its container, 
    which is this page, not down into its body, which is where this sub-form sits.
    We solve this in the next two examples.<br/><br/>
    
    References: 
    <a href="http://tapestry.apache.org/forms-and-validation.html">Forms and Validation</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/SubFormComponent1.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/component/SubFormComponent1.properties"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/component/SubFormComponent1.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/SelectPersons.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/examples/component/SelectPersons.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 SubFormComponent1 {
    static private final int MAX_RESULTS = 30;

    // Screen fields

    @Property
    private Invitation invitation;

    @Property
    private List<Person> allPersons;

    // Other pages

    @InjectPage
    private SubFormComponent2 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;
        }

        // Error if no persons chosen.

        if (invitation.getInvitedPersons().size() == 0) {
            form.recordError("You must choose at least one person to invite.");
            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.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;

public class SelectPersons {

    // 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 = "false")
    @Property
    private boolean disabled;

    // Screen fields

    @Property
    private Person person;

    // The code

    // 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;
    }

}