Knockout validation: Dynamic constraints
Asked Answered
V

3

10

I'm using Durandal, which in turn leverages off of Knockout.

I want to be able to Change validation lengths dynamically

enter image description here

Fiddle

The fiddle seems to be behaving slightly different than my "working" solution, but its still not doing what I'm wanting/expecting it to.

Viewmodel JS:

[Attempt 1]

define(function () {

   var self = this;

   self.userInfo = {       
        IdOrPassportNumber: ko.observable().extend({
            required: true,
            pattern: {
                message: 'A message',
                params: /some regex/
            }
        }),
        IdType: ko.observable()
    },

    self.isIdValid = ko.validatedObservable({ 
         IdOrPassportNumber: self.userInfo.IdOrPassportNumber 
    });

    self.userInfo.IdOrPassportNumber.subscribe(function (value) {
          if (isIdValid.isValid()) {
               console.log('YOLO!');
          }
    });

    self.userInfo.IdType.subscribe(function (value) {
        console.log(value);
        if (value === 'Passport') {
            self.userInfo.IdOrPassportNumber.extend({ maxLength: 15 });
        } else {
            self.userInfo.IdOrPassportNumber.extend({ maxLength: 13 });
        }
    });    

    var viewModel = {
        userInfo: self.userInfo
    };

    viewModel["errors"] = ko.validation.group(viewModel.userInfo);
    viewModel["errors"].showAllMessages();

    return viewModel;
});

What seems to be happening is that when i start typing i get the max & min validation of 13, but if i continue typing the validation changes to 15. I have tried another route of, setting the min & max length in the initial observable extend EG, just after the regex, and then setting the min and max length to use an observable, to no success.

[Attempt 2]

   self.userInfo = {       
       IdOrPassportNumber: ko.observable().extend({               
            maxLength: self.maxLength(), 
            minlength: self.maxLength()
       }),
       IdType: ko.observable()
   },

   self.maxLength = ko.observable();

   self.userInfo.IdType.subscribe(function (value) {

          if (value === 'Passport') {
             self.maxLength(15)
          } else {
              self.maxLength(3)
          }
    });
Velure answered 26/6, 2013 at 7:14 Comment(2)
Can you use cdnjs.com to grab the knockout validation plugin...Seductress
Done. +1 Ps very cool site. Bookmarked for sure :)Silkworm
C
5

You were so close :-) You must provide the observable itself, not the unwrapped value. So just remove the () from maxLength() - the validation library will automatically unwrap it for you.

self.userInfo = {       
   IdOrPassportNumber: ko.observable().extend({               
        maxLength: self.maxLength, 
        minlength: self.maxLength
   }),
   IdType: ko.observable()
},

Here's another example with dynamic regex patterns.

    zipPostalPattern = ko.pureComputed(() => this.countryCode() === 'US' ? '^\\d{5}(?:[-\\s]\\d{4})?$' : '');
    zipOrPostal: KnockoutObservable<string> = ko.observable('').extend(
    {
        required: true,
        pattern: {
            message: 'This is not a valid postcode for the country',
            params: this.zipPostalPattern
        }
    });

or (if you don't want a message).

    zipPostalPattern = ko.pureComputed(function() { return this.countryCode() === 'US' ? '^\\d{5}(?:[-\\s]\\d{4})?$' : ''});
    zipOrPostal: KnockoutObservable<string> = ko.observable('').extend(
    {
        required: true,
        pattern:  self.zipPostalPattern
    });

Important: If you don't want a custom message don't just remove the message parameter and leave pattern = { params: this.zipPostalPattern } because it won't work. If you don't have a message you must set the Regex/string directly for the pattern parameter.

Or of course you can just define the computed observable in place (here it's ok to call countryCode() as a function because that's how computed's work)

    zipOrPostal: KnockoutObservable<string> = ko.observable('').extend(
    {
        required: true,
        pattern:  ko.pureComputed(function() { 
                     return self.countryCode() === 'US' ? '^\\d{5}(?:[-\\s]\\d{4})?$' : ''
                  })
    });
Cipher answered 1/6, 2015 at 21:26 Comment(2)
For anyone else reading this. I'm unable to test this against this exact situation I had at this at that stage, (no longer on this project). I do prefer @Simon_Weaver's solution as the accepted solution, and will mark it as such.Silkworm
Thanks. I have to say it only occured to me today that I might be able to pass an observable as a parameter to a rule, and I got very excited - then realized perhaps it might not work - but then I got excited again that it seems to work very well :-) From what I remember it isn't clearly documented, but it's very easy if you already know how to use Knockout and adds some great flexibility.Cipher
V
6

Here is the solution that worked for me:

I made use of the custom validation feature, more specifically the single use custom validation as this wont be re-used elsewhere.

[Attempt 3]

    self.userInfo = {    
        IdOrPassportNumber: ko.observable().extend({
            required: true,
            pattern: {
                message: 'A message',
                params: /some regex/
            },
            validation: {
               validator: function (val) {
                   if (self.userInfo.IdType() === 'Id') { 
                      return val.length === 13; 
                   } else { 
                      return val.length === 15; 
                   }
                },
               message: function () {
                  if (self.userInfo.IdType() === 'Id') {
                    return 'Required: 13 characters';
                  } else {
                    return 'Required: 15 characters';
                  }
               }
            }
        })
     }
Velure answered 26/6, 2013 at 9:30 Comment(2)
As a side note. I noticed that the order of the validations also matter, so say you want your custom validator (or any other) for that matter to trigger first, order them accordinglySilkworm
Isn't it better to use two different fields with fixed validation and change binding on the page <input type="text" data-bind="value: idType()==='Id' ? id : password" />? On save just write var idOrPassword=id()||password().Ludvig
C
5

You were so close :-) You must provide the observable itself, not the unwrapped value. So just remove the () from maxLength() - the validation library will automatically unwrap it for you.

self.userInfo = {       
   IdOrPassportNumber: ko.observable().extend({               
        maxLength: self.maxLength, 
        minlength: self.maxLength
   }),
   IdType: ko.observable()
},

Here's another example with dynamic regex patterns.

    zipPostalPattern = ko.pureComputed(() => this.countryCode() === 'US' ? '^\\d{5}(?:[-\\s]\\d{4})?$' : '');
    zipOrPostal: KnockoutObservable<string> = ko.observable('').extend(
    {
        required: true,
        pattern: {
            message: 'This is not a valid postcode for the country',
            params: this.zipPostalPattern
        }
    });

or (if you don't want a message).

    zipPostalPattern = ko.pureComputed(function() { return this.countryCode() === 'US' ? '^\\d{5}(?:[-\\s]\\d{4})?$' : ''});
    zipOrPostal: KnockoutObservable<string> = ko.observable('').extend(
    {
        required: true,
        pattern:  self.zipPostalPattern
    });

Important: If you don't want a custom message don't just remove the message parameter and leave pattern = { params: this.zipPostalPattern } because it won't work. If you don't have a message you must set the Regex/string directly for the pattern parameter.

Or of course you can just define the computed observable in place (here it's ok to call countryCode() as a function because that's how computed's work)

    zipOrPostal: KnockoutObservable<string> = ko.observable('').extend(
    {
        required: true,
        pattern:  ko.pureComputed(function() { 
                     return self.countryCode() === 'US' ? '^\\d{5}(?:[-\\s]\\d{4})?$' : ''
                  })
    });
Cipher answered 1/6, 2015 at 21:26 Comment(2)
For anyone else reading this. I'm unable to test this against this exact situation I had at this at that stage, (no longer on this project). I do prefer @Simon_Weaver's solution as the accepted solution, and will mark it as such.Silkworm
Thanks. I have to say it only occured to me today that I might be able to pass an observable as a parameter to a rule, and I got very excited - then realized perhaps it might not work - but then I got excited again that it seems to work very well :-) From what I remember it isn't clearly documented, but it's very easy if you already know how to use Knockout and adds some great flexibility.Cipher
P
4

consider this

self.iDNumber = ko.observable('').extend({
        required: {
            params: true,
            message: 'ID Number is a required field.',
            insertMessages: false
        },
        SouthAfricanIDNumber: {
            message: 'Please enter a valid South African ID number.',
            onlyIf: function() {
                return self.identityType() === 'SAID';
            }
        }
    });

where SouthAfricanIDNumber is a custom validation that uses regex.

Perforated answered 22/1, 2014 at 9:37 Comment(2)
welcome to SO :), but see my accepted answer. (i'm no longer on the project. but i'll keep it in mind for the future)Silkworm
+1 to encourage spacerogue with his/her first post.Delladelle

© 2022 - 2024 — McMap. All rights reserved.