1
votes

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?

1
What do you mean that it's not binding correctly? What exactly is happening that is "wrong?" - Jeff Mercado
Good question. I don't get a binding error per se, but the control does not update on the page when the data is changed. But I just had an insight moment - could it be because by "updating the value" I mean assigning an entire new SplitDate object to the parent viewmodel, and the control WAS bound to the old one??? (Duh!) - Steve Wash

1 Answers

1
votes

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