Timo Ernst
Timo Ernst

Reputation: 15973

How to convert data between model and view in Knockout.js?

I took over a project which heavily relies on knockout.js's databinding technique. The app's purpose is basically to display (sport) courses. Required are the name of the course and the date (and time) when it begins. The data is pulled via ajax and then bound to a view via knockout.js

Basically, there is a model which looks like this:

BaseModel: function() {
    var self = this;

    var mappingOptions = {};
    self.setMappingOptions = function (options) {
        $.extend(mappingOptions, options);
    };
    self.map = function (data) {
        ko.mapping.fromJS(data, mappingOptions, self);
        return self;
    };

    self.isNew = function() {
        return !(typeof(self.id) == 'function' && self.id() != null);
    };
}

Course: function() {
    BaseModel.call(this);
    var self = this;

    ko.mapping.fromJS({
        name: null,
        date: moment().tz('UTC').minute(0).second(0).format('YYYY-MM-DDTHH:mm:ssZ'),
        timezone: null,
        duration: null
    }, {}, self);

    self.duration = ko.integerObservable();
    self.dateDateTime = new IMWeb.ko.DateTime(self.date);
    self.timezone = self.dateDateTime.timezone();
    self.dateDateTime.timezone.subscribe(function(timezone) {
        self.timezone = timezone;
    });
}

The view binds this model like this:

<input type="text" class="form-control" id="nameInput" data-bind="value: name"/>
<input type="text" class="form-control" id="dateInput" data-bind="value: clientDateDateTime.date, event:{focus: $parent.onDateInput, mouseover: $parent.onDateInput}, enable: changeable"/>
<input id="timeInput" type="text" class="form-control" data-bind="value: dateDateTime.time, event:{focus: $parent.onTimeInput, mouseover: $parent.onTimeInput}, enable: changeable">

The date in the json data which is sent from the server is set to UTC, so I created the following function in order to convert it to the client's timezone:

/**
 * Converts the given UTC date to local date of the client by subtracting
 * the local timezone offset from the given date.
 *
 * @param {Date} utcDate The date object to convert
 * @returns {Date} The converted date object
 */
var UTC2LocalDate = function (utcDate) {
    var timestamp = utcDate.getTime();              // Number of miliseconds since Jan 1st 1970, 0:00:00 UTC
    var offset = (new Date()).getTimezoneOffset();  // Local client offset in minutes
    timestamp -= offset * 60 * 1000;                // Fix date with offset by converting offset minutes to miliseconds
    return new Date(timestamp);
};

Now, here is the tricky part: How can I run this "conversion code" without interfering to much in the data binding mechanism?

Basically, I want this behavior to happen:

Upvotes: 1

Views: 929

Answers (2)

Roy J
Roy J

Reputation: 43881

Converting back and forth is best handled by a writable computed. You would have an ordinary observable for the UTC date, and a writable computed based on it. The basic construction looks like this:

vm.utcDate = ko.observable();
vm.localDate = ko.computed({
  deferEvaluation: true,
  read: function () {
    return toLocalDate(vm.utcDate());
  },
  write: function (newLocalDate) {
    vm.utcDate(toUtcDate(newLocalDate));
  }
});

(I'm obviously leaving implemention of toLocalDate andtoUtcDate up to you.) Use the localDate variable in any binding you want to display and/or accept local dates. It will automatically change when utcDate is updated, and will automatically change utcDate when a new localDate value is input.

Upvotes: 1

bartushk
bartushk

Reputation: 414

My approach would be to use your UTC2LocalDate to convert the value of self.date before being used in the construction of dateDateTime. At this point your user will be interacting with a local date time.

Next you could write a function to convert back, lets call it Local2UtcDate, and update your self.date value by subscribing to self.dateDateTime. It would look something like this:

self.duration = ko.integerObservable();
self.dateDateTime = new IMWeb.ko.DateTime(UTC2LocalDate(self.date));
self.timezone = self.dateDateTime.timezone();
self.dateDateTime.timezone.subscribe(function(timezone) {
    self.timezone = timezone;
});
self.dateDateTime.subscribe(function(changedLocalDate){
    self.date(Local2UtcDate(changedLocalDate));
});

I'm not sure exactly what the server is expecting to be posted back, but this way self.dateDateTime will be a local time and self.date will be an updated UTC time.

Upvotes: 0

Related Questions