View on GitHub

Knockout Viewmodel Plugin

Cleaner, Faster, Better Knockout Mapping

Download this project as a .zip file Download this project as a tar.gz file
Download Latest Version
(also on Nuget)

The Knockout Viewmodel Plugin (ko.viewmodel)

The fastest mapping plugin!

The knockout viewmodel plugin runs several times faster than the knockout mapping plugin. It also allows you to fine tune your viewmodel creation for even more speed. Now you can create complex observable viewmodels easily and with more structure and control than ever before!

You can trust that it works!!!

There is a growing suite of unit-tests that ensures that ko.viewmodel works reliably. Bug fixes always start with one or more new unit test so reliability goes up over time.

Creating a simple viewmodel

This code creates an viewModel with an observable array of users whose properties are observable.


model = {//your model is normally provided via an ajax call
  users:[ 
        {firstName:"John", lastName:"Doe"},
        {firstName:"James", lastName:"Smith"}
    ]
};

viewmodel = ko.viewmodel.fromModel(model);
			

Updating a viewmodel

If you need to update your viewmodel with more recent model data this can be done by calling updateFromModel.


ko.viewmodel.updateFromModel(viewmodel, updatedModel);
                

This method optionally takes a third parameter, makeNoncontiguousObjectUpdates, which can be used to eliminate long running script errors in older browsers. When set to true each object will have an update function created for it which will be called via setTimeout. Given this makeNoncontiguousObjectUpdates should only be set to true if you are getting long running script errors in older browsers. An onComplete method allows you to pass in a callback to be fired when noncontiguous object updates are completed.


ko.viewmodel.updateFromModel(viewmodel, updatedModel, true).onComplete(function(){
    //respond to completion of update
});
                

Note: the onComplete method is only available when makeNoncontiguousObjectUpdates is set to true.

Getting an updated model

At somepoint you'll need to get an updated model to send back up to the server. This can be done by calling toModel.


model = ko.viewmodel.toModel(viewmodel);
        

Extending your viewmodel

Now lets say that you want to extend each object in the users array with an isDeleted flag. You would do this by specifying an extend option.


options:{ 
    extend:{
        "{root}.users[i]": function(user){
            user.isDeleted = ko.observable(false);
        }
    }
};

viewmodel = ko.viewmodel.fromModel(model,options));

If we also wanted to add a delete method to an array we would use the following options:


options:{ 
    extend:{
        "{root}.users[i]": function(user){
            user.isDeleted= ko.observable(false);
        },
        "{root}.users": function(users){
            users.Delete = function(user){
                user.isDeleted(true);
            }
        }
    }
};

Processing Option Paths

Processing options are specified for an item by it's path. Every full path starts with {root}. Items in an array are referred to as [i]. So a path of "{root}.users[i].firstName" would be used to specify custom processing for the firstName property of every object in the users array. That is its full path name, but it's not necessary to refer to every item by it's -full path name. There are three ways of referencing a path:

  • Full Path Name - Matches only the specific path, e.g. "{root}.users[i].firstName".
  • Parent Child-Name - Matches the parent child combination specified. So "users[i].firstName" will only match the first name property on objects in the users array. Array items have to be referenced as ParentName[i].ChildName but for all other objects the proper reference is ParentName.ChildName
  • Property Name - Matches every property with that name. So a partial path of "firstName" would match every firstName property in your model.

Processing Options

There six types of processing options: custom, append, exclude, extend, arrayChildId, and shared.

  • custom - the path and all of it's children are processed only as you specify
  • append - the path and it's children are appended as is
  • exclude - the path is excluded from processing
  • extend - the path and it's children are processed normally but then extended/modified as specified
  • arrayChildId - used to identify the what property of the array's child objects should to identify them for update purposes
  • shared - allows you to reduce duplicate definitions by creating named processing functions that can be used with extend and map.

The only processing types that can be used together on a path are extend and arrayChildId. If multiple processing options are specified for the same path only one will win. Which one will win is subject to internal processing logic that has been organized for performance reasons and should not be relied upon as it is subject to change in future versions.

extend - processing option

With the extend processing option the path and it's children are processed normally but then extended/modified as specified.

options:{ 
    extend:{
        "{root}.users[i]": function(user){
            user.isDeleted = ko.observable(false);
            return user;
        }
    }
};
        

The value of the path is passed to the extend function after processing has been completed on the path and its children. Objects can be modified without being returned. Note: Whatever is returned from the extend function will be persisted and replace the default processing.

There is also this alternate syntax if you need to do unmapping:

options:{ 
    custom:{
        "{root}.users[i]": {
            map:function(mapped){
                mapped.isDeleted= ko.observable(false);
                return mapped;
            },
            unmap:function(unmapped){
            //Because isDeleted was an observable it was unwrapped and added to the model. 
                delete unmapped.isDeleted;
                return unmapped;
            }
        }
    }
};
                    
Functions added with extend will be removed when your model is created, however observables will not be and can be removed using unmap if they will cause problems with servers side serialization. It should be noted that in many cases serialization will drop unexpected properties instead of throwing an error, so this is not always necessary and would depend on your platform.

custom - processing option

The custom processing gives you complete control over mapping and unmapping from model to viewmodel and back, though as in the example below you can make additional calls to viewmodel to make things simpler. One case this is often used for is in translating back and forth from a server format (think date values) or structure to a format or structure that is easier to work with in javascript.

options:{ 
    custom:{
        "{root}.users[i]": function(user){
            user.isDeleted= ko.observable(false);
            return user;
        }
    }
};
         
There is also this alternate syntax if you need to do unmapping

options:{ 
    custom:{
        "{root}.users[i]": {
            map:function(user){
                var mapped = ko.viewmodel.fromModel(user);
                mapped().isDeleted= ko.observable(false);
                return mapped;
            },
            unmap:function(user){
                var unmapped = ko.viewmodel.toModel(user);
                delete unmapped.isDeleted;
                return unmapped;
            }
        }
    }
};
        
In this case the users are passed through as is with the addition of an observable isDeleted flag.
Note:
  • Make sure to return something from your function, otherwise undefined will be returned as the result of custom.
  • The object passed into map is the unaltered object from your viewmodel or model (depending on if you are calling fromModel or toModel).

append - processing option

With the append processing option the path and all of it's children are appended as is.

options:{ 
    append:["{root}.users[i]"]
};
        
In this case none of the users have been altered and are appended unchanged.

exclude - processing option

With the exclude processing option the path and it's children are excluded from processing and will not be included.

options:{ 
    exclude:["{root}.users[i].firstName"]
};
                
The firstName property is not included. When calling fromModel:
  • the path specified will not be wrapped in an observable.
When calling toModel:
  • if the path is for a computed value it will be unwrapped.
  • if the path is for a non-observable value it will be included.

arrayChildId - processing option

The arrayChildId option allows you to flag what the id is on an object within an array of objects so that those objects can be updated and not replaced when updateFromModel is called. If an arrayChildId is not specified then when updated data is loaded all old items will be removed from the array and new items added, which will cause you to loose the state of those objects. The syntax for this is simple:

options:{ 
    arrayChildId:{
        "{root}.users":"id"
    }
}
                
But this will make more sense in context. What if you had an array of items for purchase and the selected items were added to your total. The code might look similar to this:

var model = {
    items:[
        {
            id:256889,
            name:"item Name",
            description:"Description of Item",
            availble:3,
            price:4.25
        }
    ]
};

var viewmodel = ko.viewmodel(model, { 
    arrayChildId:{//child item id
        "{root}.items":"id"
    },
    extend:{
        "{root}.items":function(items){//Toggle function added to array for better performance
            items.ToggleSelect = function (item){
                item.selected(!item.selected);
            }
            return items;
        },
        "{root}.items[i]":function (item){
            item.selected = ko.observable(false);
        }
    }
}
                
So now imagine the app is regularly pinging the server to for an updated model so that the number of items available is accurate. Without the arrayChildId option every time an update came back the array would be populated again and all of the items would be extended with selected set to false. With the arrayChildId specified selected state of all objects will be maintained.

shared - processing option

This option allows you to define processing functions that can be reference by name in the extend and custom processing options, thus eliminating duplicate code. The following example shows a model for a movies with showtimes. The options extend both the movies and the showtimes with an observable property called selected.

var model = {
    movies:[
        {
            id:256889,
            name:"item Name",
            description:"Description of Item",
            showtimes:[
                {start:"11:45am", duration:"120 minutes", soldOut:false, pricingType:"Matinee"},
                {start:"6:45pm", duration:"120 minutes", soldOut:false, pricingType:"Evening"}
            ]
        }
    ]
};

var options = { 
    arrayChildId:{//child item id
        "{root}.movies":"id"
    },
    extend:{
        "{root}.movies[i]":"ExtendWithSelectable",//Reference shared function by name
        "{root}.movies[i].showtimes[i]": function(showtime){

            //Reference shared function directly
            options.shared.ExtendWithSelectable(showtime);

            showtime.price = ko.computed(function(){
                if(showtime.pricingType = "Matinee"){
                    return 6.75;
                }
                else if(showtime.pricingType = "Evening"){
                    return 10.25;
                }
            };
        }
    },
    shared:{
        ExtendWithSelectable:function (item){
            item.selected = ko.observable(false);
            return item;
        }
    }
}

var viewmodel = ko.viewmodel(model, options);
                
Another common use of this functionality would be if you had date, currency, or other formatting that needed to be applied to a large number of item. Or perhaps for a large number of fields you wanted "N/A" to be displayed if their were no values. This functionality could be defined once in shared and reference in many places by name.

Global Options

Global options affect all calls to ko.viewmodel and are access and set via ko.viewmodel.options. Currently there are two global options:

  • logging - (default:false) logs viewmodel processing to the console, including all paths processed and mapping options applied.
  • makeChildArraysObservable - (default:true) determines if nested arrays are converted to observable arrays. Set to false for compatibility with original mapping plugin or to improve performance.

Observable Array Mapping Methods

Once an observable array has been mapped, items which need to be added and removed can be processed through viewmodel mapping using the following methods which are added to the observable array.
  • pushFromModel - modifies the object passed in according to the original mapping options, and calls array.push
  • unshiftFromModel - modifies the object passed in according to the original mapping options and calls array.unshift
  • popToModel - calls array.pop and unmaps and returns the object according to the original mapping options
  • shiftToModel - calls array.shift and unmaps and returns the object according to the original mapping options

Think you've found a bug?

If you think you've found a bug please submit a new issue ticket . All bug fixes start with failing unit tests so including one will help speed things along... thanks!