I think the problem is twofold: there's a required validator and a validator for the submitted value. If that is the case you need to take care of both.
XPages cheats with the required validator in the sense that it attaches it dynamically in the render phase. This is because there's no validator firing in case the input value is empty for the JSF framework XPages extends from. That means that if you have a validator but you don't have required=true
on the input the validator won't run if the input value is empty.
As far as I know a custom required validator can't be used without using ugly workarounds. But as you tried to do you can play with flipping the required=
value since it will be evaluated both in the GET and POST request.
With that being said, although I'm not a fan of what I'm going to say - I would probably ask myself why I want a UX/UI done in this way, you can actually bind the buttons and work more easily with the ids. Consider the following:
<xp:div id="containerForm">
<xp:inputText id="inputText1" value="#{viewScope.whatever}"
required="#{javascript:param['$$xspsubmitid'] === getClientId(eventSubmit.id)}" />
<xp:button id="buttonSubmitValidate" value="Submit">
<xp:eventHandler id="eventSubmit" binding="#{eventSubmit}" event="onclick"
submit="true" execMode="partial" execId="containerForm" refreshMode="partial"
refreshId="messages" />
</xp:button>
<xp:button id="buttonSubmitNoValidate" value="Submit No Validate">
<xp:eventHandler event="onclick" submit="true"
execMode="partial" execId="containerForm" refreshMode="partial"
refreshId="messages" />
</xp:button>
</xp:div>
<xp:div id="messages">
<xp:messages globalOnly="false" />
</xp:div>
Here I use a couple of things inherited from the SSJS concept (sadly, but just a tad though).
In the required
property I retrieve the eventHandler client id by creating a handle to it through the binding
property. In other words that event handler component will be available under the eventSubmit
variable name. But eventSubmit.id
will return the server-side component id, which we cannot evaluate upon because the $$xspsubmitid
will contain its client-side counterpart. To get the client id for the component we then need to call getClientId(eventSubmit.id)
. At this point I have all I need to evaluate properly whether I want to require the field or not by comparing such value with what was passed in the POST request parameter $$xspsubmitid
.
The other button's event handler will certainly have a different dynamically generated client id and therefore if you submit the form by clicking on the button it's attached to it won't fire the required validator. This is the quickest way to net your result.
However, if the configuration is more complex, say you have additional validators tied to the input then I would suggest something different:
<xp:inputText id="inputText1" value="#{viewScope.whatever}"
required="#{javascript:validator.shouldBeFired(this)}" validator="#{validator.requireOtherCondition}">
<xp:this.attrs>
<xp:attr name="data-require-on-submit-id" value="#{id:eventSubmit}" />
</xp:this.attrs>
</xp:inputText>
What I try to do here is to not repeat myself. I take advantage of a validator bean to hide away the evaluation of whether the event handler id is the one I'm looking for or not. Since I will still need that event handler id the validator method I handily - this is called hijacking, LOL - store it away as an input property.
At this point the required
property and additional validator can rely on it. How?
First I just want to use a helper class for methods that can be used across my app - they might come handy elsewhere, who knows...
public enum Helper {
;
public static Attr getComponentAttr(UIComponent component, String key) {
if (component instanceof FacesAttrsObject) {
FacesAttrsObject attrsObj = (FacesAttrsObject) component;
List<Attr> attrs = attrsObj.getAttrs();
if (attrs != null) {
for (Attr attr : attrs) {
if (attr.getName().equals(key)) {
return attr;
}
}
}
}
return null;
}
@SuppressWarnings("unchecked")
public static Map<String, String> getRequestParameterMap(FacesContext facesContext) {
return (Map<String, String>) facesContext.getExternalContext().getRequestParameterMap();
}
public static String getSubmitId(FacesContext facesContext) {
return getRequestParameterMap(facesContext).get("$$xspsubmitid");
}
}
I hope the above methods result pretty explanatory.
At this point I write the validator bean methods:
public class ValidatorBean implements Serializable {
private static final long serialVersionUID = 1L;
public void requireOtherCondition(FacesContext facesContext, UIComponent component, Object value)
throws ValidatorException {
if (shouldBeFired(facesContext, component) && String.valueOf(value).length() < 3) {
FacesMessage message = new FacesMessage("Hey, it's too short!");
throw new ValidatorException(message);
}
}
public boolean shouldBeFired(UIComponent component) {
return shouldBeFired(FacesContext.getCurrentInstance(), component);
}
public boolean shouldBeFired(FacesContext facesContext, UIComponent component) {
Attr attr = Helper.getComponentAttr(component, "data-require-on-submit-id");
return attr != null && attr.getValue().equals(Helper.getSubmitId(facesContext));
}
}
The shouldBeFired
method will traverse the input attribute collection looking for an attribute named data-require-on-submit-id
. If and when it founds it, it compares it with the current submitted id retried throught the Helper class method getSubmitId
. Basically this is the same thing we did before directly on the xsp page with SSJS.
The requireOtherCondition
method is just a dumb validator that will fire only if the same condition for the required
attribute is met. If it doesn't there's no validator firing. If it does the validator is run - you can instantiate an existing one of just quickly do it there (in my case it fires if the input value length is minor than 3). See? It's as simple complicated as that.