how to use validateForm in an ajax-submitted form (popup)
komat Jun 6, 2012 12:38 PMHi :-)
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:
- I may validate multiple fields (interdependent ones as well as individual ones)
- Only those fields are set to invalid that failed validation
- All fields (including the invalid ones) keep their user-entered values upon submit (and subsequent validation failure)
Thanks for any hints !
koma