1 Reply Latest reply on Jun 11, 2012 10:55 AM by komat

    how to use validateForm in an ajax-submitted form (popup)

    komat

      Hi :-)

       

      I'm quite new to the whole JavaEE / JSF / Seam world, so I'd welcome any guidance on all aspects you might find here (best practices and so on).

       

      The basic setup is: Seam3 and PrimeFaces on JBoss AS 7.1.1 with a Postgresql 8.4 database. Project dependencies are managed with Maven 3.0.3, IDE is Eclipse 3.7 EE (Indigo) with plugins (m2e, JBoss Tools, JBoss Maven *, Seam3 Tools, ...).

       

      What I'm trying to do: I have a dataTable that displays the values of an entity (here: projects) and for each row (each project) a button that opens a popup that allows to edit the values of that project. All of this works (after tremendous amounts of pain). Now I added validation and since I have a start date and an end date for each project and end date must be after start date of course, I need to validate the two date input fields against each other. Now as I learned JSF doesn't support this, but Seam's s:validateForm does. So I implemented such a validator and it even works. But with catches:

      • Basics: The docs (http://docs.jboss.org/seam/3/faces/latest/reference/en-US/html/components.html#validateForm) tell how to specify the id of the input element with @InputField("customID") but they fail to tell how this works when injecting InputElements instead.
      • First try: as suggested in the docs, throw an Exception if the validation fails. Works, but sets all involved inputs to failed. As primefaces adds a red border around those elements it's bad from a usability standpoint (as perfectly valid stuff will look invalid).
      • Second try: As suggested by a forum post here (https://community.jboss.org/message/652804#652804) I then injected InputElements instead and set the components invalid myself, without throwing an Exception. Now only the invalid fields will render the red border. But - and there is the catch - after pressing the form submit button ("Save") and failing validation, the invalid field is reset to the backing bean value - so what the user types is lost and what is displayed isn't even invalid anymore. I believe I saw a post somewhere that this has to do with how fields are populated after a validation failure occurs, but I'm unable to find it anymore.

       

      Some code to hopefully make this a little easier to understand:

      The validator (that throws no exception, for testing purposes it fails when projectName ends with "x"):

       

      @RequestScoped
      @FacesValidator("projectValidator")
      public class ProjectValidator implements Validator {
      
          @Inject
          //@InputField("projectName")
          private InputElement<String> projectName;
      
          @Inject
          //@InputField("startDate")
          private InputElement<Date> startDate;
      
          @Inject
          //@InputField("endDate")
          private InputElement<Date> endDate;
      
      
          @Inject
          private Logger log;
      
      
          @Override
          public void validate(FacesContext context, UIComponent component, Object value) {
      
              if (projectName == null) {
                  throw new NullPointerException("Go and cry. Injection failed in seam validator.");
              } else if (projectName.getValue() == null || projectName.getValue().length() == 0) {
                  // Saw this actually happen with required="true". Rejoice !
                  FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_WARN, "Project name cannot be empty !", "A project name is required and cannot be left blank.");
                  // Next line is needed - else invalid stuff will be committed to db
                  projectName.getComponent().setValid(false);
                  //FacesContext.getCurrentInstance().addMessage(projectName.getComponent().getClientId(), msg);
                  // TODO: Next 3 lines are an ugly test replacing the line above. Unfortunately the line above doesn't work, but the ugly stuff here does.
                  String pain = /*UINamingContainer.getSeparatorChar(context) +*/ projectName.getComponent().getNamingContainer().getClientId(context) + UINamingContainer.getSeparatorChar(context) + projectName.getComponent().getClientId(context);
                  System.out.println("PAIN: " + pain);
                  FacesContext.getCurrentInstance().addMessage(pain, msg);
                  // Next line is needed - for javascript checking of validation outcome (close the popup or don't)
                  FacesContext.getCurrentInstance().validationFailed();
              } else if (projectName.getValue().endsWith("x")) {
                  FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_WARN, "Project name ends with x !", "Marvin says: This is too depressing for me to stomach.");
                  // Next line is needed - else invalid stuff will be committed to db
                  projectName.getComponent().setValid(false);
                  FacesContext.getCurrentInstance().addMessage(projectName.getComponent().getClientId(), msg);
                  // Next line is needed - for javascript checking of validation outcome (close the popup or don't)
                  FacesContext.getCurrentInstance().validationFailed();
              }
              // NOTE: endDate may be null (unset), startDate not
              //log.info("endDate: " + endDate.getValue() + ", " + endDate.getValue());
              if(endDate.getValue() != null && startDate.getValue().after(endDate.getValue())) {
                  FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_WARN, "Invalid project start/end dates !",
                          "Project end date (" + CommonsUtil.DATEFORMAT.format(endDate.getValue()) +
                          ") must be after start date (" + CommonsUtil.DATEFORMAT.format(startDate.getValue()) + ").");
                  // Next line is needed - else invalid stuff will be committed to db
                  endDate.getComponent().setValid(false);
                  FacesContext.getCurrentInstance().addMessage(endDate.getComponent().getClientId(), msg);
                  // Next line is needed - for javascript checking of validation outcome (close the popup or don't)
                  FacesContext.getCurrentInstance().validationFailed();
              }
      
              // All good here
          }
      
      }
      

       

      The button that opens the edit dialog (popup), project is the variable holding the project entity object in the current row of the datatable:

       

      <prime:commandButton value="#{bundles.messages.general_edit}"
              actionListener="#{projectController.startCreation(project)}"
              update=":editProject" oncomplete="editDialog.show()"/>
      

       

      And the form inside the primefaces edit dialog (popup) that is validated by the validator above:

       

      <!-- Using the seam3 formValidator -->
      <prime:dialog id="editDialog" header="#{bundles.messages.project_editProject}" widgetVar="editDialog" resizable="false" modal="true">
          <h:form id="editProject">
              <h:panelGrid id="grid" columns="2">
                  <h:outputLabel value="#{bundles.messages.project_name}" for="projectName" class="required"/></td>
                  <prime:inputText id="projectName" value="#{projectController.project.name}"
                          ajax="false" required="true" requiredMessage="TXTTODO Field required (Btw, this message is never displayed as the required check needs to be done in the seam validator anyway. Great stuff, this.)"
                          title="#{component.valid ? '' : 'Blahblubb !'}">
                      <c:ifMessages>
                          <pe:tooltip for="projectName" id="projectNameTooltip" position="left center" targetPosition="right center">
                              <prime:message for="projectName" id="projectNameMsg"/>
                          </pe:tooltip>
                      </c:ifMessages>
                  </prime:inputText>
      
                  <h:outputLabel value="#{bundles.messages.project_description}" for="projectDescription" class="required"/>
                  <prime:inputTextarea id="projectDescription" value="#{projectController.project.description}" ajax="false"/>
      
                  <h:outputLabel value="#{bundles.messages.project_start}" for="startDate" class="required"/>
                  <prime:calendar id="startDate" value="#{projectController.project.start}"
                          navigator="true" pattern="dd.MM.yyyy" ajax="false">
                  </prime:calendar>
      
                  <h:outputLabel value="#{bundles.messages.project_end}" for="endDate"/>
                  <prime:calendar id="endDate" value="#{projectController.project.end}"
                          navigator="true" pattern="dd.MM.yyyy" ajax="false">
                  </prime:calendar>
              </h:panelGrid>
      
              <s:validateForm validatorId="projectValidator" />
      
              <prime:commandButton id="saveProjectButton"
                      ajax="true"
                      update="@form, :projectTableForm:projectTable"
                      oncomplete="handleEditRequest(xhr, status, args)"
                      value="#{bundles.messages.general_save}"
                      action="#{projectController.completeCreation()}"
                      disabled="false"/>
              <!-- TODO: Tell bean ? (call some method that sets instance null) -->
              <prime:commandButton id="cancel" type="button" value="#{bundles.messages.general_cancel}" onclick="editDialog.hide()"/>
      
          </h:form>
      </prime:dialog>
      

       

      And the controller bean (this is probably not very interesting):

       

      @Stateful
      @ConversationScoped
      @Named
      @TransactionAttribute(TransactionAttributeType.MANDATORY) // probably unused
      public class ProjectController implements Serializable {
      
          @Inject
          private Logger log;
      
          @Inject
          private Conversation conversation;
      
          // The instance we're working with during create/edit
          private Project project;
      
          @Inject
          private ProjectDAO projectDAO;
      
      
          @Begin
          public void beginConversation() {
              // called on page load (in f:metadata via s:viewAction)
          }
      
      
          // Called when button for popup is clicked
          public void startCreation(Project project) {
              if (project == null)
                  this.project = new Project();
              else
                  this.project = project;
              log.info("Began project creation / editing BY PROJECT. Conversation: " + conversation.getId());
          }
      
      
          // Called when button for popup is clicked
          // TODO: rename (or copy and replace merge with persist), this is also used for editing
          public void startCreation(Long projectId) {
              if (projectId == null)
                  this.project = new Project();
              else
                  this.project = projectDAO.getProjectById(projectId);
              log.info("Began project creation / editing BY ID (" + projectId + "). Conversation: " + conversation.getId());
          }
      
      
          // Called when button in creation popup is clicked
          // TODO: rename (or copy and replace merge with persist), this is also used for editing
          public void completeCreation() {
              projectDAO.merge(project);
              this.project = null;
              log.info("Persisted project. Conversation: " + conversation.getId());
          }
      
      
          public Project getProject() {
              System.out.println("getProject called. project" + (project == null ? " is null." : ".name is " + project.getName()));
              return this.project;
          }
      
      
          public void setProject(Project newProject) {
              this.project = newProject;
              throw new NullPointerException("You may not call setProject. It is evil.");
          }
      
      }
      

       

       

       

      So basically the question is: How do I use the seam validateForm so that:

      1. I may validate multiple fields (interdependent ones as well as individual ones)
      2. Only those fields are set to invalid that failed validation
      3. All fields (including the invalid ones) keep their user-entered values upon submit (and subsequent validation failure)

       

       

      Thanks for any hints !

      koma

        • 1. Re: how to use validateForm in an ajax-submitted form (popup)
          komat

          So in the end it turns out it was sort of my own fault. Removing the <c:ifMessages> facet made it work like I intended (well, apart from filling in the backing bean value if I delete all characters from the projectName input field and so submit an empty value - but I guess I can live with that). The ifMessages thigy is intended to only render the tooltip when there are FacesMessages for the component (in this case the projectName inputText). It works, but apparently also causes weird behaviour. The idea by Martin Ahrer is from this blog post: http://www.martinahrer.at/2009/10/20/control-rendering-of-ui-elements-based-on-validation-results/ . If someone wants to take a look at why this happens, here is the code that I use (mostly what Martin Ahrer posted, so all credits to him):

           

           

          public class IfMessagesTagHandler extends TagHandler {
          
              private static final String FOR_ATTRIBUTE_NAME = "for";
              private final TagAttribute forAttribute;
          
              public IfMessagesTagHandler(TagConfig config) {
                  super(config);
                  forAttribute = getAttribute(FOR_ATTRIBUTE_NAME);
              }
          
              public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, FaceletException,
                      ELException {
                  
                  if (forAttribute != null) {
                      // looked up in Mojarra's ComponentStateHelper
                      // Code doesn't work.
                      throw new UnsupportedOperationException("Sorry, you cannot use for in ifMessages at the moment, the code for this doesn't work.");
                  } else if (parent != null) {
                      if (ctx.getFacesContext().getMessages(parent.getClientId(ctx.getFacesContext())).hasNext()) {
                          this.nextHandler.apply(ctx, parent);
                      }
                  } else {
                      throw new UnsupportedOperationException("Neither a for-attribute was specified nor could a parent be found for ifMessages.");
                  }
              }
          
          }