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