First some concepts that I have developed while working this problem:
- Field- anything that will ultimately be wrapped in a ko.observable or some extended version.
- View Model Item Objects- a object with Fields or Collections
- View Model Collection Objects- ko.observable or some extended version.
Some ideas I wanted to plan for:
- Easy to update Field level sub properties (makes it easy to add change tracking or accept/cancel)
- Ability to add shared functionality across View Model Item and Collections Objects
- Easy to add type checking at a top level.
- A control freak's alternative to ko.mapping.
- Allow for reususe of View Models if the shape you are passing down is the same and the behavior is the same or can easily be extended to suit multiple use cases.
First we will look at the item factory. This is what will be instantiated to make new View Model Item Objects within our view model. For simple examples you may just new one of these at the top level. It is also possible to composite together multiple calls to this factory to get more complex view model structures.
Sample 1: View Model Item Factory
core.models.viewModelItemFactory = function (itemInit) {
var mapper = function (data) {
itemInit(data, this, ko.observable);
this.toJson = function () {
return ko.toJSON(this);
}
};
return mapper;
};
Basically there are two parts to this factory:
- A itemInit function is passed in that will handle Field initialization. Notice we pass ko.observable in to that function. If later we want to have other properties or functions hung off each field we could change that in one place for the entire application.
- We also wrap toJSON from ko. This means we can keep our view models dumb of our usage of Knockout.js and will just have to change the implementation if we ever switch over to another library.
Sample 2: View Model Item Factory Usage and Instancing
core.profile.profileItemViewModel =
core.models.viewModelItemFactory(
function (input, output, field) {
output.fname = field(input.fname);
output.lname = field(input.lname);
output.line1 = field(input.line1);
output.line2 = field(input.line2);
output.city = field(input.city);
output.state = field(input.state);
output.zip = field(input.zip);
});
var currentProfile = {
fname: "Joshua",
lname: "Kincaid",
line1: "42 A Street",
city: "Norfolk",
state: "Va",
zip: "23502"
};
$(function () {
var vm = new core.profile.profileItemViewModel(currentProfile);
ko.applyBindings(vm, $('#address')[0]);
ko.applyBindings(vm, $('#formatted')[0]);
});
The usage shown in Sample 2 drives the similarity in complexity to any of the samples that more tightly couple view model creation to definition.
For View Model Collections we just pass the Item View Model in as a mapper.
Sample 3: View Model Collection Factory Usage
core.profile.profileViewModel = core.models.viewModelCollectionFactory(core.profile.profileItemViewModel);
With the implementation I again wanted to divorce my View Model definitions from Knockout.js, so I changed some of the structure of the view model object to show this. Here are the functions:
- items - the traditional ko.observableArray although there it could be changed to anything else in one line of code instead of being riddled throughout the application.
- add - a spiced up version of the ko version. Supports splice as an insert. Also will automatically call the View Model Item mapper if it is a pure JSON object instead of the expected view model object.
- remove - a wrapper around the ko version.
- toJson - a convienence object for converting the collection to JSON.
Sample 4: View Model Collection Factory
core.models.viewModelCollectionFactory =
function (itemMapper) {
var mapper = function(data) {
this.items= ko.observableArray(ko.utils.arrayMap(data, function (item) { return new itemMapper(item); }));
this.add= function (data, index) {
var vm = data instanceof itemMapper ? data : new itemMapper(data);
if (typeof index === 'undefined') {
this.items.push(vm);
} else {
this.items.splice(index, 0, vm);
}
};
this.remove = function (item) {
this.items.remove(item);
};
this.toJson= function () {
return ko.toJSON(this.items);
};
};
return mapper;
};
The usage is exactly the same as the Item example.
Sample 5: View Model Collection Instancing Usage
var currentProfile = [{
fname: "Joshua",
lname: "Kincaid",
line1: "42 A Street",
city: "Norfolk",
state: "Va",
zip: "23502"
},
{
fname: "Lars",
lname: "Aldritch",
line1: "430 21st Street",
city: "Norfolk",
state: "Va",
zip: "23505"
},
{
fname: "Valtino",
lname: "Landry",
line1: "98 Chesapeake Ave",
city: "Norfolk",
state: "Va",
zip: "23513"
},
{
fname: "Jeremy",
lname: "Kilpatrick",
line1: "490 Atlantis Street",
city: "Newport News",
state: "Va",
zip: "23602"
}];
$(function () {
var data = new core.profile.profileViewModel(currentProfile);
var vm = { contacts: data };
core.currentProfileVM = vm;
ko.applyBindings(vm, $('#people')[0]);
});
These samples were all pulled from a presentation I did at work on this topic. I have stuck them up on github.
No comments:
Post a Comment