Search code examples
javascriptknockout.jsknockout-validation

How To Attach Knockout Validation To A Custom Binding


I have created a Knockout custom binding to format dates using moment.js.

ko.bindingHandlers.dateStringValue = {
    update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var value = valueAccessor(),
        allBindings = allBindingsAccessor();
        var valueUnwrapped = ko.utils.unwrapObservable(value);

        if (valueUnwrapped === null) {
            ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor, viewModel);
            $(element).val("");
        } else {
            ko.bindingHandlers.value.update(element, valueAccessor, allBindingsAccessor, viewModel);
            var pattern = allBindings.datePattern || "MM/DD/YYYY";
            $(element).val(moment(valueUnwrapped).format(pattern));
        }
    }
};

This works great with my binding:

<input type="text" data-bind="dateStringValue: entryDate, datePattern: 'M/D/YYYY'" />

I created a custom validation rule from code I found on the web.

// http://stackoverflow.com/a/23086828/139917
// Use: var someDate= ko.observable().extend({ simpleDate: true });
// Dependencies: date.js
ko.validation.rules['simpleDate'] = {
    validator: function (val, validate) {
        var d = Date.parse(val);
        return (d != null);
        //return ko.validation.utils.isEmptyVal(val) || moment(val, 'MM/DD/YYYY').isValid();
    },
    message: 'Invalid date entry date'
};

ko.validation.registerExtenders();

I tried both moment.js and date.js date libraries.

I attached the custom validation rule to my viewmodel observable.

self.entryDate = ko.observable(je.entryDate || new Date()).extend({ simpleDate: true });

Now I want to use Knockout Validation to validate input but don't know how to let the validation framework know about my new binding. It seems Knockout Validation only knows about some or all of the built in Knockout bindings.

Calling ko.validation.makeBindingHandlerValidatable("dateStringValue"); raises a 0x800a138f - JavaScript runtime error: Object expected error in Knockout v3.2.0. It has been suggested that calling into the Knockout value binding from my custom binding would work but code like ko.bindingHandlers.value.update(element, valueAccessor); have not worked for me.

How can I let Knockout Validation know about my custom binding?


Solution

  • Here's how I solved my problem. I created a read/write computed property on my entity and used moment.js to validate the input:

    var dateFormatString = "M/D/YYYY"
    
    self.entryDate = ko.observable(je.entryDate || new Date());
    self.formattedEntryDate = ko.computed({
        read: function () {
            return moment(self.entryDate()).format(dateFormatString);
        },
        write: function (value) {
            var momentDate = moment(value, dateFormatString);
            if (momentDate.isValid()) {
                self.entryDate(momentDate.format(dateFormatString));
                self.isValidEntryDate(true);
                return;
            }
            self.isValidEntryDate(false);
        },
        owner: self
    });
    self.isValidEntryDate = ko.observable(true);
    

    If the input string was not in the expected format, I would set an isValidEntryDate observable to false which would display a message to the user prompting them to re-enter the date.

    <input type="text" data-bind="value: formattedEntryDate" />
    <div class="validationMessage" data-bind="hidden: isValidEntryDate">Invalid entry date. Use M/D/YYYY format.</div>
    

    This binding uses a custom binding handler of hidden.

    I realize this doesn't address the question but in the name of productivity, this accomplishes the goal.

    Feel free to answer the question directly.