Skip to content

Instantly share code, notes, and snippets.

@sody
Last active December 16, 2015 11:09
Show Gist options
  • Save sody/5425817 to your computer and use it in GitHub Desktop.
Save sody/5425817 to your computer and use it in GitHub Desktop.
Form separation in Tapestry
<fieldset>
<legend>${message:section.common}</legend>
<t:label for="first" class="control-label"/>
<t:textfield t:id="first" value="firstName" validate="required,maxLength=50"
label="message:label.first-name"/>
<t:label for="last" class="control-label"/>
<t:textfield t:id="last" value="lastName" validate="required,maxLength=50"
label="message:label.last-name"/>
</fieldset>
@SupportsInformalParameters
public class FormFieldSet {
@Parameter(required = true, defaultPrefix = BindingConstants.LITERAL)
private String title;
@Inject
private ComponentResources resources;
@Environmental
private ValidationTracker tracker;
@BeginRender
void renderTitle(final MarkupWriter writer) {
writer.element("fieldset");
resources.renderInformalParameters(writer);
// render legend
final Element el = writer.element("legend");
if (tracker.getHasErrors()) {
el.addClassName("text-error");
}
writer.write(title);
writer.end();
}
@AfterRender
void end(final MarkupWriter writer) {
writer.end();
}
}
public class FormSection {
private static final ComponentAction<FormSection> SETUP_TRACKER = new SetupTracker();
private static final ComponentAction<FormSection> CLEANUP_TRACKER = new CleanupTracker();
private static final ComponentAction<FormSection> PROCESS_SUBMISSION = new ProcessSubmission();
@Persist(PersistenceConstants.FLASH)
private ValidationTracker tracker;
private ValidationTracker wrapper;
@Inject
private ComponentResources resources;
@Environmental
private FormSupport formSupport;
@Environmental
private TrackableComponentEventCallback eventCallback;
@Inject
private Environment environment;
@SetupRender
void storeSetupAction() {
formSupport.storeAndExecute(this, SETUP_TRACKER);
}
@CleanupRender
void storeCleanupAction() {
formSupport.storeAndExecute(this, CLEANUP_TRACKER);
formSupport.store(this, PROCESS_SUBMISSION);
}
private void processSubmission() {
// defer validation to be executed after all field values are populated
formSupport.defer(new Runnable() {
public void run() {
environment.push(ValidationTracker.class, wrapper);
validateSection();
environment.pop(ValidationTracker.class);
}
});
}
private void setupValidationTracker() {
// get or create inner validation tracker
// it will save validation state for fields within this form section
final ValidationTracker innerTracker = tracker != null ? tracker : new ValidationTrackerImpl();
// get original validation tracker
final ValidationTracker outerTracker = environment.pop(ValidationTracker.class);
// add error recording hook to original validation tracker
// that will save inner validation tracker in session flash attribute
final ValidationTracker outerTrackerWrapper = new ValidationTrackerWrapper(outerTracker) {
private boolean saved = false;
private void save() {
if (!saved) {
tracker = innerTracker;
saved = true;
}
}
@Override
public void recordError(final Field field, final String errorMessage) {
super.recordError(field, errorMessage);
save();
}
@Override
public void recordError(final String errorMessage) {
super.recordError(errorMessage);
save();
}
};
// replace original validation tracker with its hooked version
environment.push(ValidationTracker.class, outerTrackerWrapper);
// create composite validation tracker that will record errors and input values
// in both inner and original validation trackers
wrapper = new ValidationTrackerWrapper(innerTracker) {
@Override
public void recordError(final Field field, final String errorMessage) {
super.recordError(field, errorMessage);
outerTrackerWrapper.recordError(field, errorMessage);
}
@Override
public void recordError(final String errorMessage) {
super.recordError(errorMessage);
outerTrackerWrapper.recordError(errorMessage);
}
@Override
public void recordInput(final Field field, final String input) {
super.recordInput(field, input);
outerTrackerWrapper.recordInput(field, input);
}
};
// push composite tracker to environment
// to be accessible by all inner components
environment.push(ValidationTracker.class, wrapper);
}
private void cleanupValidationTracker() {
// pop composite tracker from environment
environment.pop(ValidationTracker.class);
}
private void validateSection() {
try {
// trigger validate event
// it will record error if validation exception occurs
resources.triggerEvent(EventConstants.VALIDATE, null, eventCallback);
} catch (RuntimeException ex) {
final ValidationException ve = ExceptionUtils.findCause(ex, ValidationException.class);
if (ve != null) {
wrapper.recordError(ve.getMessage());
return;
}
throw ex;
}
}
static class SetupTracker implements ComponentAction<FormSection> {
public void execute(final FormSection component) {
component.setupValidationTracker();
}
@Override
public String toString() {
return "FormSection.SetupTracker";
}
}
static class CleanupTracker implements ComponentAction<FormSection> {
public void execute(final FormSection component) {
component.cleanupValidationTracker();
}
@Override
public String toString() {
return "FormSection.CleanupTracker";
}
}
static class ProcessSubmission implements ComponentAction<FormSection> {
public void execute(final FormSection component) {
component.processSubmission();
}
@Override
public String toString() {
return "FormSection.ProcessSubmission";
}
}
}
<t:form t:id="form" class="form-horizontal" autocomplete="off">
<t:form.section t:id="commonSection">
<t:form.fieldset title="message:section.common">
<t:form.errors/>
<div class="control-group">
<t:label for="first" class="control-label"/>
<div class="controls">
<t:textfield t:id="first" value="firstName" validate="required,maxLength=50"
label="message:label.first-name"/>
</div>
</div>
<div class="control-group">
<t:label for="last" class="control-label"/>
<div class="controls">
<t:textfield t:id="last" value="lastName" validate="required,maxLength=50"
label="message:label.last-name"/>
</div>
</div>
<div class="control-group">
<t:label for="about" class="control-label"/>
<div class="controls">
<t:textarea t:id="about" value="about" validate="maxLength=255"
label="message:label.about"/>
</div>
</div>
</t:form.fieldset>
</t:form.section>
<t:form.section t:id="credentialsSection">
<t:form.fieldset title="message:section.credentials">
<t:form.errors/>
<div class="control-group">
<t:label for="password" class="control-label"/>
<div class="controls">
<t:passwordfield t:id="password" value="password" validate="required"
label="message:label.password"/>
</div>
</div>
<div class="control-group">
<t:label for="confirmPassword" class="control-label"/>
<div class="controls">
<t:passwordfield t:id="confirmPassword" value="confirmPassword" validate="required"
label="message:label.confirm-password"/>
</div>
</div>
</t:form.fieldset>
</t:form.section>
<div class="form-actions">
<t:submit value="message:label.submit" class="btn btn-primary"/>
</div>
</t:form>
public class FormSection {
private static final ComponentAction<FormSection> PROCESS_SUBMISSION = new ProcessSubmission();
@Inject
private ComponentResources resources;
@Environmental
private ValidationTracker tracker;
@Environmental
private FormSupport formSupport;
@Environmental
private TrackableComponentEventCallback eventCallback;
@CleanupRender
void storeCleanupAction() {
formSupport.store(this, PROCESS_SUBMISSION);
}
private void processSubmission() {
// defer validation to be executed after all field values are populated
formSupport.defer(new Runnable() {
public void run() {
validateSection();
}
});
}
private void validateSection() {
try {
// trigger validate event
// it will record error if validation exception occurs
resources.triggerEvent(EventConstants.VALIDATE, null, eventCallback);
} catch (RuntimeException ex) {
final ValidationException ve = ExceptionUtils.findCause(ex, ValidationException.class);
if (ve != null) {
tracker.recordError(ve.getMessage());
return;
}
throw ex;
}
}
static class ProcessSubmission implements ComponentAction<FormSection> {
public void execute(final FormSection component) {
component.processSubmission();
}
@Override
public String toString() {
return "FormSection.ProcessSubmission";
}
}
}
@OnEvent(value = EventConstants.VALIDATE, component = "credentialsSection")
void validateCredentials() throws ValidationException {
if (password != null && confirmPassword != null && !password.equals(confirmPassword)) {
throw new ValidationException(message("error.password-match"));
}
}
<t:form.section t:id="credentialsSection">
<t:label for="password" class="control-label"/>
<t:passwordfield t:id="password" value="password" validate="required"
label="message:label.password"/>
<t:label for="confirmPassword" class="control-label"/>
<t:passwordfield t:id="confirmPassword" value="confirmPassword" validate="required"
label="message:label.confirm-password"/>
</t:form.section>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment