Search code examples
javascriptvalidationknockout.jsknockout-validation

Knockout-Validation multiple custom async rules


I have a domain property and I want to validate two things;

  1. URL exists (is reachable)
  2. URL exists in my local DB.

In order to check these things I created to async validation rules using https://github.com/Knockout-Contrib/Knockout-Validation and applied both of them on my property.

What happens is that each time the response from one of the rules comes earlier and it sets isValidating property to false and I want this property to be true until the response from my second rule came.

  1. Custom rules:

     export function enableCustomValidators() {
        (ko.validation.rules as any)["urlValidationServicePath"] = {
        async: true,
        validator: function (url: string, baseUrl: string, callback: any) {
            getRequest(url, baseUrl, callback, "true");
        },
        message: 'You must enter a reachable domain.',
    },
    (ko.validation.rules as any)["customerValidationServicePath"] = {
        async: true,
        validator: function (url: string, baseUrl: string, callback: any) {
            getRequest(url, baseUrl, callback, "false");
        },
        message: "This url already exists in our system. Please contact us at [email protected]",
    }
    
    ko.validation.registerExtenders();
    }
    
    function getRequest(url: string, baseUrl: string, callback: any, method: string) {
        var restClient = new RestClient();
        restClient.downloadString(baseUrl.concat(url), (responseText) => {
            method === "true" ? callback(responseText === "true" ? true : false) :
                callback(responseText === "true" ? false : true);
    });
    }
    
  2. Using of the rules:

    export class CompanySetupVM extends BasePageVM {
        public websiteUrl: KnockoutObservable<string> = ko.observable(undefined);
        public isValidating: KnockoutObservable<boolean> = ko.observable(false);
    
        public constructor() {
            this.websiteUrl.extend({
                required: {
                params: true,
                message: CompanySetupVM.ErrorMessageNullWebsiteUrl
            },
            urlValidationServicePath: CompanySetupVM.DomainValidationPath,
            customerValidationServicePath: CompanySetupVM.CustomerValidationPath
            });
            this.isValidating = ko.computed(() => this.websiteUrl.isValidating(), this);   
        }
    }
    
  3. In cshtml:

     data-bind="text: currentPage().nextButtonText, css: {'button-overlay': currentPage().isValidating(), 'button': !currentPage().isValidating()}, click: nextAction"
    

Solution

  • I've looked at the source code of knockout validation (here) and it's pretty clear that two independent async validators are not supported.

    The isValidating property is set to true as soon as an async rule is begins to run and set to false again as soon as that rule finishes. Therefore, multiple async rules clash.

    There is only one solution. Remove the second async validator.

    You can collapse the two checks into one either on the client side or on the server side.

    To do it on the client side, you would need to write a validator that runs two Ajax requests and invokes the validation callback only after both of them have returned.

    To do it on the server side, you would have to run the "is reachable" and "is in DB" checks in succession before giving an overall response to the client.

    Personally I would prefer changing the server side, because

    1. it keeps the client code tidy and manageable
    2. it saves one HTTP round-trip per check
    3. semantically, the URL check is one thing that fail for more than one reason
    4. it's easy to let the server send a custom validation result and -message

    Besides plain true or false, the validation plugin understands responses in this format:

    {isValid: false, message: "something is wrong"}
    

    So make your server send a JSON response with the appropriate validation result and error message and your REST client download JSON instead of text.

    Then all you need to do is pass the server's response directly to the validation callback.

    ko.validation.rules.urlValidationServicePath = {
        async: true,
        validator: function (url, baseUrl, callback) {
            restClient.downloadJSON(baseUrl.concat(url), callback);
        },
        message: 'The URL you entered is not valid.'
    };
    

    Here the message is only a default. The server's message always takes precedence over the setting in the validation rule.