Search code examples
knockout.jsknockout-validation

knockout-validation: binding to validationMessage changes


I'm working on a form that uses knockout validation and I have a confirmPassword input that is associated with a span that displays the validation message (i.e. I have insertMessages set to false in ko.validation.init). I want a slide-in animation on this span, so it's wrapped in a div that has a custom binding to change the height (i have a css transition set on the height):

<div class="slideOuter">
    <div class="slideInner" data-bind="slideElement: confirmPassword.isModified() & !confirmPassword.isValid()">
        ...
        <span class="form-control" data-bind="validationMessage: confirmPassword"></span>
        ...

The binding code:

ko.bindingHandlers.slideElement = {
    update: function (element, valueAccessor, allBindings) {
        // First get the latest data that we're bound to
        var value = valueAccessor();

        // Next, whether or not the supplied model property is observable, get its current value
        var valueUnwrapped = ko.unwrap(value);

        var $outer = $(element).closest('.slideOuter');
        var $inner = $(element).closest('.slideInner');

        if (valueUnwrapped == true) {
            // Make the element visible
            $(element).css('display', 'block');
            setTimeout(function () { $outer.css('height', $inner.outerHeight(true) + "px"); }, 0);
        }
        else {
            // Make the element invisible
            $(element).css('display', 'none');
            $outer.css('height', "0px");
        }
    }
};

The problem is getting this custom binding to fire when the validationMessage changes. This setup works great as long as the validationMessage stays the same, but with the confirmPassword, I have two different messages depending on the error:

self.confirmPassword = ko.observable("").extend(
   {
       required:
           {
               params: true,
               message: "Confirm Password is required"
           },
       equal:
           {
               params: self.password,
               message: "The confirmation does not match the password"
           }
    });

With the space I have on the form, the first message occupies only one line and the second message occupies two lines. The problem is that since the custom binding does not fire if switching between the two messages, the height of my slide div doesn't compensate for the new size of the validationMessage. (i.e.: First i don't put in any confirm password so the shorter message is the validationMessage and the slideElement custom binding fires and sets the div to the appropriate height. Then I change the confirm password to something other than the password, it doesn't match so the second message becomes the new validationMessage and shows up in the div... but the custom binding doesn't fire and the height of the div doesn't compensate.)

Does anyone know how I can get an event to fire when the validationMessage changes?


Solution

  • I have found a way to make this workable, but it is hardly elegant. It seems that v2.0 of knockout.validation may make this scenario easier by making observable.error observable, but in the meantime, you can solve a problem like mine by making a second custom binding for observables that have multiple validationMessages and you need to do something when the message changes:

    View Model addition:

    self.confirmPassword.ValMsg = null;
        self.confirmPassword.ValMsgUpdate = ko.observable(false);
        self.confirmPassword.subscribe(function (newValue) {
            if (self.confirmPassword.ValMsg != self.confirmPassword.error)
            {
                self.confirmPassword.ValMsg = self.confirmPassword.error;
                self.confirmPassword.ValMsgUpdate(true);
                self.confirmPassword.ValMsgUpdate(false);
            }
        });
    

    Html update:

    <div class="slideInner" data-bind="slideElement: confirmPassword.isModified() & !confirmPassword.isValid(), slideElementUpdate: confirmPassword.ValMsgUpdate()">
    

    Custom binding addition:

    ko.bindingHandlers.slideElementUpdate = {
            update: function (element, valueAccessor, allBindings) {
                // First get the latest data that we're bound to
                var value = valueAccessor();
    
                // Next, whether or not the supplied model property is observable, get its current value
                var valueUnwrapped = ko.unwrap(value);
    
                if (valueUnwrapped == true) {
                    var $outer = $(element).closest('.slideOuter');
                    var $inner = $(element).closest('.slideInner');
                    $outer.css('height', $inner.outerHeight(true) + "px");
                }
            }
        };