2

I am validating a user entered account number using two validators, one for basic standard format, and the other that validates the account number against values stored in a database. The database of valid account numbers may not always be up to date so I want to allow the user to override and submit their entered account number but only after the database validation has failed. I always want to validate its standard format 8 characters with no spaces.

    <h:form id="formId">
        <p:panelGrid>
            <p:row>
                <p:column>
                    <p:outputLabel value="Account : " for="acct" />
                </p:column>
                <p:column>
                    <p:selectOneMenu id="acct" value="#{bean.acct.acctNum}" effect="fold" editable="true" validator="acctLengthAndSpaceValidator" required="true" requiredMessage="Required">

                        <f:selectItems value="#{bean.mySavedAccounts}" var="acct"
                            itemLabel="#{acct.acctNumber} itemValue="#{acct.acctNumber}" />
                        <o:validator validatorId="accountDatabaseValidator" disabled="#{bean.skipDbValidation}" />
                    </p:selectOneMenu>
                </p:column>
                <p:column>
                    <p:messages for="acct" showDetail="true" skipDetailIfEqualsSummary="true" />
                </p:column>
            </p:row>
        </p:panelGrid>
        <br />

            
        <p:selectBooleanCheckbox rendered="#{facesContext.validationFailed}" value="#{bean.skipDbValidation}" itemLabel="I know this account is really valid, please skip validation and let me submit!">
            <p:ajax update="@this" listener="#{bean.testListener()}" />
        </p:selectBooleanCheckbox>

        <p:commandButton value="Submit" action="#{bean.submit()}" update="formId"/>
    </h:form>

The checkbox does appear after the form is initially submitted and has any validation failure (I will figure out how to isolate to just the failed accountDatabaseValidator). But then when I select the checkbox, and submit again, both validators are still fired. I added the ajax listener to debug, and it isn't firing and the boolean value skipDbValidation is still false.

Perhaps my approach is not correct in achieving my concrete goal of validating against the database but then giving the user the option of skipping the db validation after initial failure.

EDIT if i remove rendered="#{facesContext.validationFailed}" from the checkbox and have it visible all the time, the boolean skipDbValidation will get set to true if the checkbox is checked and then on subsequent submit, the skipDbValidation is ignored as expected. But I do not want the checkbox allowing the user to bypass visible at first. Only after validation fails.

jeff
  • 3,618
  • 9
  • 48
  • 101

2 Answers2

3

The technical explanation that this doesn't work is that the rendered attribute is re-evaluated during processing the form submit. At this point the faces context is not validationFailed anymore (it was only validationFailed during the previous request) and thus the component is not rendered anymore and then the component's submitted value won't be applied. This matches #6 of commandButton/commandLink/ajax action/listener method not invoked or input value not set/updated.

Your work around by rendering it client-side rather than server-side is acceptable. But I gather that you wanted to show it only when the specific validator has been invoked. There are at least 2 ways to achieve this:

  1. Check UIInput#isValid() of the input component of interest. You can achieve that by binding the component to the view (not to the bean!) via component's binding attribute so you can reference it elsewhere in the same view.

     <p:selectOneMenu binding="#{acct}" ...>
         ...
     </p:selectOneMenu>
    
     ...
    
     <p:selectBooleanCheckbox styleClass="#{acct.valid ? 'ui-helper-hidden' : ''}" ...>
         ...
     </p:selectBooleanCheckbox>
    

    Note that I took the opportunity to reuse the PrimeFaces-provided style class.

  2. Or, make the validator a view scoped bean and reference it via <o:validator binding> instead.

     @Named
     @ViewScoped
     public class AccountDatabaseValidator implements Validator, Serializable {
    
         private boolean validationFailed;
    
         @Override
         public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException {
              // ...
    
              validationFailed = !isValid(value);
    
              if (validationFailed) {
                  throw new ValidatorException(createError("Invalid value"));
              }
         }
    
         public boolean isValidationFailed() {
             return validationFailed;
         }
     }
    
     <p:selectOneMenu ...>
         <o:validator binding="#{accountDatabaseValidator}" ... />
     </p:selectOneMenu>
    
     ...
    
     <p:selectBooleanCheckbox rendered="#{accountDatabaseValidator.validationFailed}" ...>
         ...
     </p:selectBooleanCheckbox>
    
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Never got around the other day to posting my solution but this was how I ended up displaying checkbox only after specific failure. style="#{facesContext.messageList.stream().anyMatch(v -> v.summary == 'Invalid Account') or bean.skipDbValidation ? 'Display: inline' : 'Display: none;'}" It seems to be working, not sure if there are any downsides to it. – jeff Sep 10 '20 at 12:54
  • 1
    It'll match the same message of potential another validator in same view. Plus, the message is hardcoded. When you or your successor later change the message in the validator, you'll have to remember to also change the one in the view. More prone to regression and tech debt thus. – BalusC Sep 10 '20 at 13:05
  • Yep, you are right about both scenarios. Since I am currently the only developer and only option I could get to work, I was willing to accept those shortcomings. But I will give your approach a try. Thanks! – jeff Sep 10 '20 at 13:19
  • Sure, you're the only developer, but you can be [hit by a bus](https://en.wikipedia.org/wiki/Bus_factor) tomorrow. – BalusC Sep 10 '20 at 13:21
1

My work around to get the checkbox to programmatically display and so the checkbox would function was to hide and display using CSS instead of the render attribute.

style="#{facesContext.validationFailed ? 'Display: inline' : 'Display: none;'}"

    <p:selectBooleanCheckbox style="#{facesContext.validationFailed ? 'Display: inline' : 'Display: none;'}" value="#{bean.skipDbValidation}" itemLabel="I know this account is really valid, please skip validation and let me submit!">
        <p:ajax update="@this" />
    </p:selectBooleanCheckbox>

But I still can't figure out how to display the checkbox for a specific validation failure.

I will post another question for that

EDIT Here is how I ended up displaying the checkbox only after the Invalid Account validation failure.

    <p:selectBooleanCheckbox style="#{facesContext.messageList.stream()
         .anyMatch(v -&gt; v.summary == 'Invalid Account') or 
          bean.skipDbValidation ? 'Display: inline' : 'Display: none;'}" 
          value="#{bean.skipDbValidation}" itemLabel="I know this account is really valid, please skip validation and let me submit!">
        <p:ajax update="@this" />
    </p:selectBooleanCheckbox>
jeff
  • 3,618
  • 9
  • 48
  • 101