Steve Wash
Steve Wash

Reputation: 986

Custom Knockout binding for Compound Control

I’m looking for an example (jsfiddle if possible) of a custom binding for what we used to call a “compound component” years ago. I really don’t want someone to write my code for me. I just want to look over a few samples before I invest a lot of time in this, but I can't find any. Most custom binding examples are one way only. Just asking if you are aware of a sample that is similar …

Most of HTML/CSS is given to me by a design team so the layout isn’t always my choice. In this case there are dates, phone numbers and social security inputs that are all created using a common “theme”. That theme is to have three separate elements within a div. For example, the date has one for month, one for day and one for year. (I don’t believe we don’t need to complicate this with them being spinners). Validations and max/min input restrictions are expected.

I created an object as follows

var SplitDate = function (date) {
    var self = this;
    self.month = ko.observable();
    self.day = ko.observable();
    self.year = ko.observable();

    var momentDate = moment(date);

    if (momentDate.isValid()) {
        self.month(momentDate.month() + 1);
        self.day(momentDate.day());
        self.year(momentDate.year());
    }
}

In my viewmodel, I have tried both

self.dateOfBirth = new SplitDate(myDate);

and

self.dateOfBirth = ko.observable(new SplitDate(myDate));

but neither of those binds correctly using a standard value binding such as

data-binding = "value: dateOfBirth.day" or data-binding = "value: dateOfBirth().day"

So I'm presuming I need a custom binding. I'm not sure what's the best approach to take and if all of these need to be observables or not. We're using Knockout validations so I also expect I'll be adding a function for isValid() to the SplitDate.

So my question is, before I spend hours fumbling around with this, does anyone have a good example?

Upvotes: 1

Views: 151

Answers (1)

Jeff Mercado
Jeff Mercado

Reputation: 134921

Your SplitDate object is tracking month, day and years as separate components. It sounds to me like you're trying to set them all using a full date and are having troubles. I suspect you're doing something like this:

self.dateOfBirth = new SplitDate(newDate);

This will not work, your view is bound to the observables in the previous SplitDate. What you need to do is update the date components.


However, I think you're going about this wrong. The split date should be a wrapper around a full date, not just some date components. You'll just need to provide accessors to the different fields you want to support. In knockout terms, you'll want an observable for the full date, then have computed observables for the different fields.

function SplitDate(date) {
    var _date = ko.observable(); // the backing field
    setDate(date);

    // we can intercept set attempts to validate
    this.date = ko.computed({ read: _date, write: setDate });
    this.year = ko.computed({ read: makeGetter('year'), write: makeSetter('year') });
    // we need to special case months to offset to be 1-based
    this.month = ko.computed({ read: getMonth, write: setMonth });
    // the date is the day of the month, day is day of the week
    this.day = ko.computed({ read: makeGetter('date'), write: makeSetter('date') });

    function setDate(date) {
        var momentDate = moment(date);
        if (momentDate.isValid()) _date(momentDate);
    }
    function makeGetter(field) {
        var getter = moment.fn[field];
        return function () {
            var date = _date();
            if (date) return getter.call(date);
        };
    }
    function makeSetter(field) {
        var setter = moment.fn[field];
        return function (value) {
            var date = _date();
            if (date) {
                setter.call(date, value);
                // we modified the underlying value, notify
                _date.valueHasMutated();
            }
        };
    }
    // we need to offset months
    function getMonth() { // 1-12
        var date = _date();
        if (date) return date.month() + 1;
    }
    function setMonth(month) { // 1-12
        var date = _date();
        if (date) {
            date.month(month - 1);
            _date.valueHasMutated();
        }
    }
}

With this, to change the value of any components of the date as if they are regular observables.

self.dateOfBirth = new SplitDate(date);
self.dateOfBirth.month(8); // August
self.dateOfBirth.date(newDate);

You should be able to bind to the fields separately and any updates on the fields will update the underlying date as you would expect.

<input type="text" data-bind="value: dateOfBirth.date"/>
<input type="text" data-bind="value: dateOfBirth.year"/>
<input type="text" data-bind="value: dateOfBirth.month"/>
<input type="text" data-bind="value: dateOfBirth.day"/>

fiddle

Upvotes: 1

Related Questions