This Confluence has been LDAP enabled, if you are an ASF Committer, please use your LDAP Credentials to login. Any problems file an INFRA jira ticket please.

Child pages
  • Conditional Validation
Skip to end of metadata
Go to start of metadata

Flagging a form field as "required" is the most common kind of validation. In most cases, this can be specified statically as follows:

add(new TextField("foo").setRequired(true));

In some cases, however, whether or not the field is required cannot be determined when the form is created. Like many properties in Wicket, we can override the property getter (isRequired, in this case) to defer the evaluation of the property until the last possible moment:

add(new TextField("foo") {
    public boolean isRequired() {
        // return true if the field is required
    }
}

That's the simple part. Unfortunately, implementing isRequired can be tricky, since it's called very early in the form processing cycle, before values are converted and models are updated. Below we provide a few recipes that cover some common scenarios.

Submit Button

In this recipe, the field is in a form with multiple submit buttons. We only want to require the field when one of the buttons is clicked:

Button submit = new Button("submit") {
    public void onSubmit() {
        // handle form submission
    }
}
form.add(submit);

TextField foo = new TextField("foo") {
    public boolean isRequired() {
        Form form = (Form) findParent(Form.class);
        return form.getRootForm().findSubmittingButton() == button;
    }
}
form.add(foo);

Note the call to getRootForm. Technically, this is only required in nested forms.

If you would like to bypass validation altogether you can do so by:

new Button("submit").setDefaultFormProcessing(false);

Validating just the button-press

Simply add a validator to the button which checks if it was pressed:

button.add(new IValidator<String>() {
        @Override
        public void validate(IValidatable<String> validatable) {
          if (button.getForm().findSubmittingButton() == button) {
            // ... do your test here
            if (errorFound) {
              ValidationError error = new ValidationError().addMessageKey("errorFound_button");
              error.setVariables(Collections.singletonMap("parameter", (Object) "value"));
              validatable.error(error);
            } 
          }
        }
      });

Alternative Approach

Another approach to enabling validation based on which submit button was used is to take over the form processing workflow as follows.

Say we have four form components within the same form:

  1. name A
  2. description A
  3. name B
  4. description B

Sometimes we want the whole form to validate/process (Button C), but sometimes we only want "A" form components to validate/process (Button A). One way to accomplish this is to:

final TextField nameA = new TextField("name", ...);
final TextField descriptionA = new TextField("descriptionA", ...);
final TextField nameB = new TextField("nameB", ...);
final TextField descriptionB = new TextField("descriptionB", ...);

// the validators can be anything, but for simplicity we just use required
nameA.setRequired(true);
descriptionA.setRequired(true);
nameB.setRequired(true);
descriptionB.setRequired(true);

final Button buttonA = new Button(id) {
    public boolean onSubmit() {
        // because we overrode the form processing we need to handle validation/processing on the components ourselves
        nameA.validate();
        descriptionA.validate();
        if(!nameA.isValid() || !descriptionA.isValid()){
                // didn't validate so we stop processing (validation errors will be displayed provided we are using a FeedbackPanel or similar)
                return;
        }
        // TODO : now we have the updated values/models so we can perform whatever button A is supposed to do
    }
});
// set the form processing to false so that no validation/processing will occur on the form when button A is clicked
buttonA.setDefaultFormProcessing(false);

// we can do the same thing for B components
final Button buttonB = new Button(id) {
    public boolean onSubmit() {
        // because we overrode the form processing we need to handle validation/processing on the components ourselves
        nameB.validate();
        descriptionB.validate();
        if(!nameB.isValid() || !descriptionB.isValid()){
                // didn't validate so we stop processing (validation errors will be displayed provided we are using a FeedbackPanel or similar)
                return;
        }
        // TODO : now we have the updated values/models so we can perform whatever button B is supposed to do
    }
});
// set the form processing to false so that no validation/processing will occur on the form when button B is clicked
buttonB.setDefaultFormProcessing(false);

final Button buttonC = new Button(id) {
    public boolean onSubmit() {
        // TODO : all validation passed because form processing is true so we can perform whatever button C is supposed to do
    }
});

CheckBox

In this recipe, the field is only required when a checkbox on the form is checked:

final CheckBox checkBox = new CheckBox("cb");
form.add(checkBox);

TextField foo = new TextField("foo") {
    public boolean isRequired() {
        return Strings.isTrue(checkBox.getValue());
    }
}
form.add(foo);

Radio Button

Here the field is only required when a particular radio button on the form is selected:

final RadioGroup radioGroup = new RadioGroup("radioGroup");
form.add(radioGroup);

final Radio radio1 = new Radio("radio1");
radioGroup.add(radio1);

TextField foo = new TextField("foo") {
    public boolean isRequired() {
        return Strings.isEqual(radioGroup.getInput(), radio1.getValue());
    }
}
form.add(foo);

DropDownChoice

Normally, you give a list of domain objects to a DropDownChoice. We can check which one was selected with the getConvertedInput() method as follows:

public class DocumentType {
    private String name;
    private boolean hasExpiryDate;
    // getters and setters omitted
}

List<DocumentType> allDocumentTypes = ...;

final DropDownChoice ddc = new DropDownChoice("documentType", allDocumentTypes, new ChoiceRenderer("name"));
form.add(ddc);

TextField expiryDate = new TextField("expiryDate", Date.class) {
    public boolean isRequired() {
        DocumentType dt = (DocumentType) ddc.getConvertedInput();
        return dt != null && dt.getHasExpiryDate();
    }
}
form.add(expiryDate);

Isolating Nested Forms

Consider a nested form with its own submit button. When this button is clicked, only components on the nested form are validated. However, when a button on the parent form is clicked, components on both the parent form and the child form are validated.

Occasionally, you may want the forms to be truly independent, i.e. you want the parent form to not validate and submit the child form.

class InternalForm<T> extends Form<T> implements IFormVisitorParticipant {
    public InternalForm(String name) {
        super(name);
    }

    public InternalForm(String name, IModel<T> model) {
        super(name, model);
    }

    public boolean processChildren() {
        IFormSubmittingComponent submitter = getRootForm().findSubmittingButton();
        if (submitter == null)
            return false;
        
        return submitter.getForm() == this;
    }
}

Form nestedForm = new InternalForm("nestedForm");
  • No labels

2 Comments

  1. Submit button example is actually a bit incomplete. Normally on the forms onSubmit you would like to access the textfield but you really cant with the skeleton code as you run into both components referring each other.

    1. Actually, most times I don't access text fields in onSubmit(), since by then the text field has updated its model object, so I can access the model object directly.

      However, if you really need to access the text field from onSubmit, I suggest making either the submit button or the text field (or both) fields in the containing component.