In the not too distant past, this was something that used to plague me too.
Why?
Quite simply, for 2 reasons.
The first reason was I didn't really understand JavaScript from an object point of view. To me it had always been a "Scripting Language" and that was it, I'd heard people tell me otherwise, but I was (still am) a C# person, so I largely ignored what was said because it didn't look like C# objects.
All that changed, when I started to use Type Script, and TS showed me in the form of it's JS output what object JS looked like.
The second reason was the very flexibility of JS and more importantly knockout.
I loved the idea of simply just changing my JSON endpoint in C# code, and not really having to make any changes in the front end other than a few tweaks to my HTML.
If I wanted to add a new field to an output object, then I could, followed by simply adding a new column to a table, binding the correct field name and job done.
Fantastic right.
Indeed it was, until I started getting asked to provide row based functionality. It started simple with requests like being able to delete rows and do things like inline editing.
That lead to many strange adventures such as this:
Knockout JS + Bootstrap + Icons + html binding
and this:
Synchronizing a button class with an option control using knockoutjs
But the requests got stranger and more complex, and I started coming out with ever crazy ways of traversing the DOM, to get from the row I was on, up to the parent and back down to my sibling, then yanking the text from adjacent elements and other craziness.
Then I saw the light
I started working on a project with a junior dev who only ever knew and/or lived in JS land, and he simply asked me one question.
"If it's all causing you so much pain, then why don't you just make a view model for your rows?"
And so the following code was born.
var DirectoryEntryViewModel = (function ()
{
function DirectoryEntryViewModel(inputRecord, parent)
{
this.Pkid = ko.observable(0);
this.Name = ko.observable('');
this.TelephoneNumber = ko.observable('');
this.myParent = parent;
ko.mapping.fromJS(inputRecord, {}, this);
}
DirectoryEntryViewModel.prototype.SomePublicFunction = function ()
{
// This is a function that will be available on every row in the array
// You can call public functions in the base VM using "myParent.parentFunc(blah)"
// you can pass this row to that function in the parent using "myParent.someFunc(this)"
// and the parent can simply do "this.array.pop(passedItem)" or simillar
}
return DirectoryEntryViewModel;
})();
var IndexViewModel = (function ()
{
function IndexViewModel(targetElement)
{
this.loadComplete = ko.observable(false);
this.phoneDirectory = ko.observableArray([]);
this.showAlert = ko.computed(function ()
{
if (!this.loadComplete())
return false;
if (this.phoneDirectory().length < 1)
{
return true;
}
return false;
}, this);
this.showTable = ko.computed(function ()
{
if (!this.loadComplete())
return false;
if (this.phoneDirectory().length > 0)
{
return true;
}
return false;
}, this);
ko.applyBindings(this, targetElement);
$.support.cors = true;
}
IndexViewModel.prototype.Load = function ()
{
var _this = this;
$.getJSON("/module3/directory", function (data)
{
if (data.length > 0)
{
_this.phoneDirectory(ko.utils.arrayMap(data, function (item)
{
return new DirectoryEntryViewModel(item, _this);
}));
} else
{
_this.phoneDirectory([]);
}
_this.loadComplete(true);
});
};
return IndexViewModel;
})();
window.onload = function ()
{
var pageView = document.getElementById('directoryList');
myIndexViewModel = new IndexViewModel(pageView);
myIndexViewModel.Load();
};
Now this is not by any stretch of the imagination the best example (I've just yanked it out of a project I had to hand), but it works.
Yes, you have to make sure that if you add a field to your backend in the JSON, that you also add it to the row view model (Which you can load with RequireJS or similar) , as well as adding that column to your table/list/other markup where needed.
But for the sake of an extra line or two, you get to add a function once, easily, that then is available on every row in the collection.
And, just on a note of typescript, the entire JS above was generated by the TS compiler, from a TS source that implements the same pattern.
I have quite a few things running in this manner (There are a couple of examples on my github page - http://github.com/shawty you can clone), some of the apps I have running like this have entire view-models that adapt an entire UI based on a single simple change in a computed observable, I have rows that manage their own state (Including talking directly to the database) then update their state in the parent table once an operation has been successful.
Hopefully that will give you some more food for thought. While you can probably wrestle on down the road your taking, trying to extend the existing KO classes, I think in all honesty you'll find the above pattern much easier to get to grips with.
I tried the same approach as you once over, but I abandoned it fairly quickly once my JS friend pointed out how easy it was to just create a VM for the row and re-use it.
Hope it helps
Shawty