7

I'm using Knockout.Validation and I'd like to be able to display an error summary where each line displays the error message (obviously!) and also the field name on the view model related to it, e.g.

  • Age - please enter a number
  • Date of birth - please enter a proper date

So far I've got a validatedObservable wrapping my view model, and this puts an errors array on my viewmodel automatically, containing all my errors. However I can't see any easy way to retrieve which field each error relates to.

I know I could traverse the view model myself, building up an errors collection of my own from the isValid property - is this my only option though?

Once I have the field name, I can map my validation summary to the related "friendly" label for that field (e.g. "Date of birth" rather than "DateOfBirth").

Here's a simplified version of the code I have so far:

ViewModel

function PersonModel(){
   var self = this;
   self.Age = ko.observable().extend({ number: true});

   self.DateOfBirth = ko.observable({ date: true});             
   self.Validate = function() {                           
       if (!self.isValid()) {                                         
          self.errors.showAllMessages();        
          return false;          
       }
       return true;
    };    

ko.validation.init({
                grouping: { deep: true, observable: true },
                registerExtenders: true,
                messagesOnModified: true,
                insertMessages: true
            });

ko.applyBindings(ko.validatedObservable(new PersonModel()));

Html

<ul data-bind='foreach: model().errors' class="message">
    <li>
           <span data-bind='text:  $data'></span>
    </li>
</ul>

Many Thanks

philicomus
  • 133
  • 1
  • 9

6 Answers6

7

You can use custom validation message for any variable.

emailAddress: ko.observable().extend({
    required: { message: 'Email Address: Required field.' }
}),
amakhrov
  • 3,820
  • 1
  • 13
  • 12
  • this falls apart when you have two of the same thing being validated - for instance what about a billing and shipping address which both have the same fields – Simon_Weaver Jun 20 '14 at 07:55
  • 3
    They shouldn't be the same observables, knockout validation requires that you extend each individual (observable), which will be validated. You can possibly use onlyif validation rule for postal addresses or some other custom rules https://github.com/Knockout-Contrib/Knockout-Validation/wiki/User-Contributed-Rules. – kasperoo Jun 20 '14 at 16:06
7

You can do the following:

  • Add a friendlyName extender to provide a friendly name to your observables
  • Hack the binding handler that shows the messages

Friendly Name:

Simplest knockout extender ever:

ko.extenders.friendlyName = function (obs, options) {
    obs.friendlyName = options;
}

Usage: self.Age = ko.observable(3).extend({ friendlyName: "My Age" });

Display message hack:

Knockout validation plugin creates a bindinghandler validationMessage to display error messages. You can use it directly (with some html markup) or let the plugin handle the messages with the configuration option insertMessages.

Here I just edit the html it creates (the original binding handler is still called) to take the friendlyName into account:

var originalValidationMessageUpdate= ko.bindingHandlers.validationMessage.update;
ko.bindingHandlers.validationMessage.update = 
    function (element, valueAccessor, allBindingAccessor, viewModel, 
              bindingContext) {
        if (originalValidationMessageUpdate)
            originalValidationMessageUpdate(element, valueAccessor, 
                                            allBindingAccessor, viewModel,
                                            bindingContext);
        var prepend = (ko.isObservable(valueAccessor()) 
                            && valueAccessor().friendlyName)
                                  ? valueAccessor().friendlyName + " - "
                                  : "";

        $(element).html(prepend + $(element).html());
        // ^^^^ This is the line that is actually useful
    }

Note: I did not create friendlyName as an observable since I guess it will not be updated, but it is possible to do it.

Demo

GôTô
  • 7,974
  • 3
  • 32
  • 43
  • I'm going to go with this idea I think, so thanks for that. It's a shame they can't put something in Ko.Validation to help though - might suggest it! – philicomus Jun 24 '14 at 09:30
  • @philicomus Well, you could [contribute](https://github.com/Knockout-Contrib/Knockout-Validation) :) – GôTô Jun 24 '14 at 09:35
3

I have found this works well - type it in the console (F12):

for(var propertyName in PersonModel()) {
  console.log(ko.validation.group(PersonModel()[propertyName])())
}

This will give you something like:

[]
[]
[]
[This field is required.]
[]
[]

You can then tie up the problem field to the property in your model. In this example, the problem would be the 4th property.

I hope this helps someone.

Sean Thorburn
  • 1,728
  • 17
  • 31
2

Unfortunately you will need to specify messages for each validator unless you do something entirely custom such as GôTô has suggested.

Think about it... you haven't specified a display name for you fields so how is ko validation supposed to know a field name for the validation error message?

The simplest way:

self.Age = ko.observable().extend({ 
   number: {
      params: true,
      message: 'Age - please enter a number'
   },
   required: { 
      params: true,
      message: 'Age is required'
   }
});

If it justifies the effort, you may want to invest in extending the default messaging behaviour else its easier to just specify the messages for each validator on each property.

Daniel Kereama
  • 911
  • 9
  • 17
  • I think that does work for simple view models, where the fields are static, but I'm actually coding a framework which has metadata for the various fields, including display/friendly name. So I really need something at the framework level which applies ko.extenders to my (dynamic) objects based on this meta data, and to avoid duplication, this needs to be done as generically as possible. Therefore I'll probably go with something based on GôTô 's idea, but with the addition of also adding the friendly name to the errors collection too. – philicomus Jun 24 '14 at 09:29
  • @philicomus your original question was almost a year ago - what were you doing in the meantime? – Simon_Weaver Jun 24 '14 at 19:00
  • Server validation for one thing! :) I saw validation on the server as the highest priority and additional client-side validation as a nice-to-have. As it's a framework that I've been developing, I've been working on implementations of new apps using the framework. – philicomus Jun 25 '14 at 09:39
1

This minimal overhead approach is ideal for my needs:

ko.validation.rules["required"].message = "{0} required";

usage:

self.driverName = ko.observable().extend({ required: "Driver Name" });

rendered result:

Driver Name required

Note: stepping through knockout.validation.js showed me it's crucial to modify the default ko.validation.rules[] PRIOR to any ko.observable().extend() calls.

Beej
  • 794
  • 8
  • 15
1

While this doesn't answer the "friendly" label portion of this question, nor does it result in an array of validation errors, it does build up an object to let you see where in a huge viewmodel validation errors are hiding. The question mentions traversing through the viewmodel to build up the errors, so I figured I'd share the recursive function I built for doing that with some help from Lodash.

function getErrors(koVm, maxDepth, currentDepth) {
    if ((currentDepth || 0) < (maxDepth || 5)) {
        var obj = _.pickBy(_.mapValues(koVm, function(value) {
            if (value) {
                if (_.isFunction(value.error)) {
                    return value.error();
                }

                return getErrors(value, maxDepth, (currentDepth || 0) + 1);
            }

            return null;
        }));

        return _.keysIn(obj).length ? obj : null;
    }

    return null;
}

It's not great for showing errors to users (unless you flatten the constructed object), but it's been a great help when debugging and trying to figure out what part of a huge model isn't valid.

CosworthTC
  • 358
  • 5
  • 15