I’ve been using KnockoutJS for a long time now, but only recently found the magic that is the Knockout-Mapping plugin.
A few days ago, I spent some time refining some old code, and encountered something along the following lines:
function report(reportData) {
var self = reportData;
self.StartedText = friendlyPastMoment(reportData.Started);
if (reportData.Ended == 0001-01-01T00:00:00) {
self.EndedText = 'Still running...';
}
else {
self.EndedText = friendlyPastMoment(reportData.Ended);
}
return self;
}
(The friendlyPastMoment()
method is just a small wrapper method around MomentJS, for handling edge cases)
We had a couple more properties handled similarly. Now, this worked just fine when the information displayed to the end-user was static – we just added a few text binding to a couple of span nodes, and the StartedText
and EndedText
were displayed beautifully. But we’ve decided that we want to update the information displayed to the user automatically, which meant that I now had to replace those simple StartedText
and EndedText
properties into Knockout computed properties. But for that to work, I had to change the implementation – the Started
and Ended
properties of the report need to be observable properties.
I’ve started with a naïve implementation:
function report(reportData) {
var self = reportData;
self.Started = ko.observable(reportData.Started);
self.Ended = ko.observable(reportData.Ended);
self.StartedText = ko.computed(function() { return friendlyPastMoment(self.Started()) });
self.EndedText = ko.computed(function() {
if (self.Ended() == "0001-01-01T00:00:00") {
returna 'Still running...';
}
else {
return friendlyPastMoment(self.Ended());
}});
return self;
}
But since we had several properties in reportData
that needed to implement this behavior, this started to become a code-smell. I was certain that there must be a better way of handling these kinds of scenarios.
The Knockout-Mapping plugin was created exactly for such cases – it converts a simple POCO object into its Knockout equivalent, with all properties implemented as observable properties, with arrays and all:
function report(reportData) {
ko.mapping.fromJS(reportData, {}, this);
var self = this;
self.StartedText = ko.computed(function() { return friendlyPastMoment(self.Started()) });
self.EndedText = ko.computed(function() {
if (self.Ended() == "0001-01-01T00:00:00") {
return 'Still running...';
}
else {
return friendlyPastMoment(self.Ended());
}});
return self;
}
As you can see, the first line calls the fromJS
method, which takes a regular JavaScript object, an options object (empty {}
in the line above), and the destination object. The result will be that this object will have the same properties as reportData
, implemented as Knockout observable properties.
Now, it is very easy to add a refresh function to report, which will retrieve the most current information from the server, and update the report:
self. Refresh = function() {
$.getJSON(api/report?id= + self.Id(), function (data) {
ko.mapping.fromJS(data, self);
});
};
As you can see, we’re making a getJSON
call to the server, and once we get the data back, we just call the fromJS
method again, this time passing the data retrieved from the server, and our current report instance.