Sub-Form As a Field (1)

The sub-form in this example does its own validation AND it links to its label because it extends AbstractField 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.

Create Invitation

This solution does not handle Tapestry and JSR-303 validators. For a comprehensive solution see Tapestry's Checklist component: Checklist.tml, Checklist.java.

References: AbstractField, 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 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 &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 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}&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.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;
    }

}