jwaliszko / ExpressiveAnnotations

Annotation-based conditional validation library.
MIT License
351 stars 123 forks source link

Validate DropDownListFor Using ClientSide Validation #166

Closed sricard closed 7 years ago

sricard commented 7 years ago

I have a form mixed with different controls, TextBoxes, CheckBoxes and DropDownLists. When I check form validity in javascript as shown in the snippet below the validation works properly for TextBoxes but not for the DropDownLists. It seems to always pass validation.

For this field that is bound to the control, I am using the RequiredIf attribute which works correctly after post to the server; but I need the validation to work client side.

[DisplayName("Refused")]
[RequiredIf("SelectedStatusCode == 'REFUSD'", ErrorMessage = "A refused shipment status is required.")]
public string Refused { get; set; }

$(function () {
    $("#ApplyStatusButton").click(function () {
        if (!$("#UpdateStatusForm").valid()) {
            return false;
        }
    });
});
jwaliszko commented 7 years ago

Have you tried to troubleeshot it? Would you mind providing the relevant part of the view which is causing the problems?

sricard commented 7 years ago

I'm not sure what there is for me to troubleshoot. If you can give me instructions on where to look, I would be happy to dig into the 0's and 1's. :)

Here is the declaration of one of the DropDownLists.

<tr>
    <td>
        <h5>@Html.LabelFor(m => m.UpdateStatus.Refused)</h5>
    </td>
    <td>
        @(Html.Kendo().DropDownListFor(m => m.UpdateStatus.Refused)
            .OptionLabel(" ")
            .BindTo(new List<SelectListItem> {
                new SelectListItem() { Text = "Delivery Attempted", Value = "Y" },
                new SelectListItem() { Text = "No Delivery Attempted", Value = "N" }
                }))
        @(Html.ValidationMessageFor(m => m.UpdateStatus.Refused, "", new { @class = "p-validation-error" }))
    </td>
</tr>
jwaliszko commented 7 years ago

EA library has not been tested (and is not intended to be) with HTML helpers provided by 3rd party vendors. All supported tests are executed using ASP.NET MVC built-in helpers.

Nevertheless, are you sure the Kendo helper renders the EA-compatible HTML, i.e. containing data-val-* attributes? You should expect an output very similar to that one below:

<select data-val="true" 
            data-val-requiredif="?" 
            data-val-requiredif-allowempty="false" 
            data-val-requiredif-expression="&quot;SelectedStatusCode == 'REFUSED'&quot;" 
            data-val-requiredif-fieldsmap="{&quot;SelectedStatusCode&quot;:&quot;string&quot;}" 
            id="UpdateStatus_Refused" 
            name="UpdateStatus.Refused" 
            ...>
    <option></option>
    <option value="Y">Delivery Attempted</option>
    <option value="N">No Delivery Attempted</option>
</select>
sricard commented 7 years ago

Looks like it renders it differently but the attributes appear to be there.

<tr>
    <td>
        <h5><label for="UpdateStatus_Refused">Refused</label></h5>
    </td>
    <td>
        <span title="" class="k-widget k-dropdown k-header ea-triggers-bound" unselectable="on" role="listbox" aria-haspopup="true" aria-expanded="false" tabindex="0" aria-owns="UpdateStatus_Refused_listbox" aria-disabled="false" aria-busy="false" aria-activedescendant="f19d9521-f435-49ca-95be-29b3638db6e1" style=""><span unselectable="on" class="k-dropdown-wrap k-state-default"><span unselectable="on" class="k-input"> </span><span unselectable="on" class="k-select" aria-label="select"><span class="k-icon k-i-arrow-60-down"></span></span></span><input data-val="true" data-val-requiredif="A refused shipment status is required." data-val-requiredif-allowempty="false" data-val-requiredif-expression="&quot;SelectedStatusCode == 'REFUSD'&quot;" data-val-requiredif-fieldsmap="{&quot;SelectedStatusCode&quot;:&quot;string&quot;}" id="UpdateStatus_Refused" name="UpdateStatus.Refused" type="text" required="required" validationmessage="Required" class="ea-triggers-bound" data-role="dropdownlist" style="display: none;"></span><script>
    jQuery(function(){jQuery("#UpdateStatus_Refused").kendoDropDownList({"dataSource":[{"Text":"Delivery Attempted","Value":"Y"},{"Text":"No Delivery Attempted","Value":"N"}],"dataTextField":"Text","dataValueField":"Value","optionLabel":" "});});
</script>
        <span class="field-validation-valid p-validation-error" data-valmsg-for="UpdateStatus.Refused" data-valmsg-replace="true"></span>
    </td>
</tr>
jwaliszko commented 7 years ago

Maybe it's not directly related, but I've noticed your HTML contains these attributes: required="required" validationmessage="Required". This is redundant since you need conditional requirement - remove them because it's contradictory.

Also, please open the browser web console (F12), and verify whether EA triggers validation when the SelectedStatusCode field value is changed. In order to do it, firstly, set the debug flag to true:

<script>
    ea.settings.debug = true;

Next, filter-out the relevant SelectedStatusCode-related activity:

image

If everything works as expected, you should be able to see some debug messages informing you that EA has detected dependent field, and is going to trigger validation for it (your messages will be slightly different though, because I'm using the latest sources).

If the messages are not there (or error appears), something's not right. Maybe the input field the Kendo is generating using its JavaScript code, is hidden? Hidden fields are ignored by default by jquery-validation plugin, and EA follows analogic rules. If this is true, try to enable validation for hidden fields (empty-out the ignore filter), i.e.:

<script>
    var validator = $('form').validate();
    validator.settings.ignore = ''; // enable validation for ':hidden' fields

and check out the console again.

sricard commented 7 years ago

Thanks for the detailed response. The additional required attributes are there for a workaround in case we can't get this figured out in time. I have temporarily removed them to try out your steps.

Here is the newly rendered HTML for the DropDownList.

<tr>
    <td>
        <h5><label for="UpdateStatus_Refused">Refused</label></h5>
    </td>
    <td>
        <span title="" class="k-widget k-dropdown k-header ea-triggers-bound" unselectable="on" role="listbox" 
            aria-haspopup="true" aria-expanded="false" tabindex="0" aria-owns="UpdateStatus_Refused_listbox" aria-disabled="false" 
            aria-busy="false" aria-activedescendant="3b8bdc39-ff8a-4017-b374-5a19e2d28103" style="">
            <span unselectable="on" class="k-dropdown-wrap k-state-default">
                <span unselectable="on" class="k-input"> </span>
                <span unselectable="on" class="k-select" aria-label="select">
                    <span class="k-icon k-i-arrow-60-down"></span>
                </span>
            </span>
            <input data-val="true" data-val-requiredif="A refused shipment status is required." 
            data-val-requiredif-allowempty="false" 
            data-val-requiredif-expression="&quot;SelectedStatusCode == 'REFUSD'&quot;" 
            data-val-requiredif-fieldsmap="{&quot;SelectedStatusCode&quot;:&quot;string&quot;}" 
            id="UpdateStatus_Refused" name="UpdateStatus.Refused" type="text" 
            class="ea-triggers-bound" data-role="dropdownlist" style="display: none;">
        </span>
        <script>
            jQuery(function(){jQuery("#UpdateStatus_Refused").kendoDropDownList({"dataSource":[{"Text":"Delivery Attempted","Value":"Y"},{"Text":"No Delivery Attempted","Value":"N"}],"dataTextField":"Text","dataValueField":"Value","optionLabel":" "});});
        <script>
        <span class="field-validation-valid p-validation-error" data-valmsg-for="UpdateStatus.Refused" data-valmsg-replace="true"></span>
    </td>
</tr>

I placed the script to enable validation of hidden fields, it did not seem to have any impact and there were no errors generated from it. The only thing that stood out because of your comment about not validation hidden fields was that the input field appears to be hidden based on the display:none attribute. Thoughts on that?

Attached are the results from the development tools client debugger. localhost-1500299766229.TXT

jwaliszko commented 7 years ago

From what I understand, it is the SelectedStatusCode field which should trigger validation for your UpdateStatus.Refused dropdown, and highlight it when requirement condition is satisfied.

I cannot see such an activity in your log file. When changing the SelectedStatusCode field value, the following traffic should be recorded:

Dependency validation trigger - keyup event, handled.
Validation triggered for following UpdateStatus.SelectedStatusCode dependencies: UpdateStatus.Refused.
RequiredIf expression of UpdateStatus.Refused field:
SelectedStatusCode == 'REFUSD'
will be executed within following context (methods hidden):
{
    "SelectedStatusCode": "REFUS"
}
Dependency validation trigger - keyup event, handled.
Validation triggered for following UpdateStatus.SelectedStatusCode dependencies: UpdateStatus.Refused.
RequiredIf expression of UpdateStatus.Refused field:
SelectedStatusCode == 'REFUSD'
will be executed within following context (methods hidden):
{
    "SelectedStatusCode": "REFUSD"
}

Lack of such traces in the log says the validation is not being executed when SelectedStatusCode is being modified. How the SelectedStatusCode is rendered? Does the UpdateStatus.Refused validation behave properly when you submit the entire form?

I was able to reproduce your scenario and assuming hidden fields validation is enabled, it works as expected.

sricard commented 7 years ago

The validation works perfectly server-side.

When I am loading up the view/form, generally the SelectedStatusCode is passed in via JSON / QueryString so it doesn't usually change once the form is loaded. There is a scenario where the SelectedStatusCode can be chosen in a DropDownList, but none of the special statuses are available in that list because they each have special requirements, which is what we are trying to validate.

Here are some screenshots of what is happening. When the form passes validation client-side we are showing the waiting indicator and then the post happens. Because the validation fails server-side the waiting indicator stays resident.

refused1 refused2

jwaliszko commented 7 years ago

If it's not a problem, it would be very beneficial to have a tiny, yet complete sample project, where this issue would be clearly isolated and reproducible. I'm afraid I'm unable to help without such an archive attached here.

sricard commented 7 years ago

Here is a sample. Hit submit without filling out any information and you'll see the Status Notes does not pass validation. Put in some text for Status Notes and you'll see the validation passes client side even though the Refused drop down needs to be populated. eaValidationSample.zip

jwaliszko commented 7 years ago

Hi, thank you for the sample.

The issue is as predicted above, mainly :hidden fields validation is off by default. When enabled, it works fine (edit your Index.cshtml):

image

image

Maybe you've faced some timing-related issue when turning this option to early? DOM needs to be ready for the form to be found.

sricard commented 7 years ago

Thank you! That did the trick.