Project Description

knockout-data-projections (aka ko-data-projections) makes it easier for developers to translate complex view models to plain javascript objects.
It can be used in any js-to-js one-to-one nested or flat mappings and it can handle knockout observables well.

Overview

Working with knockout (http://knockoutjs.com/) system is extremely useful for any ajax enabled rich application. We create complex view models which are projected through knockout system to the user interface.

Problems arise at the stage of load/save of viewmodels from server as the plain javascript objects have to be translated from and to knockout view models. Those could be complex having observable collections, helper observables, nested observables etc.

I am aware of 3 major approaches to handle this:

  1. Use ko.toJS to make the viewmodel plain object and manually afterwards change the object
  2. Use Object.prototype.toJSON as stated here http://www.knockmeout.net/2011/04/controlling-how-object-is-converted-to.html by Ryan Niemeyer and implement self translating logic inside each viewmodel object
  3. Use knockout-mappings http://knockoutjs.com/documentation/plugins-mapping.html


This approach resembles knockout-mappings (view model to js) but with the use of schema object

knockout-data-projections helps developers project the viewModel to plain javascript object by defining a schema. The schema is a plain javascript object which will be the guide for the translation. The name of each property from the object will be used to map the viewmodel object, and the data type of the entered value will be the target value (converted if needed). If the relevant source viewmodel property is null, the schema value can be used to be the resulting value. Schema objects can be nested and can include arrays, with the declaration of each array item as the first element in the schema array.

In detail:

  • The schema can be nested, defining inner properties and schemas.
  • Every property existing in the schema will be translated to the defined schema data type and injected into the resulting object.
  • ko.observable() and ko.observablearray() properties will be unwrapped to plain properties, included arrays
  • Properties not existing in the schema will be ignored.
  • Schema properties not existing into view model can be translated to various results.
  • Array properties can be presented by inserting into the schema an array with one element that has the schema of the array elements
  • null and undefined data can be handled custom by inserting directions in array style of [data type,null value, undefined value]
  • user can extend the current view model with custom data that does not exist
  • A schema property can be a function that returns the projected data. That function has the following parameters:
function project(data, parent, root){
     data=> is the data to project
     parent=> is the parent object that contains this data property
     root=> is the root object (the object that the user requested to project initially)

     returns<= the projected data
}

 

Examples

Having the following view model:

function Address() {
	this.id = 5;
	this.street = ko.observable();
	this.postalcode = ko.observable();
	this.isGreek = ko.observable();
}

function Person(fn,ln) {
	this.firstName = '';
	this.lastName = ko.observable(ln);

	this.addresses = ko.observablearray();
}


and some time the person instance has data according to this:

person = {
	firstName:"George",
	lastName:"Mavritsakis",
	addresses:[
		{street:"street1",postalCode:"code1",id:3,isGreek:'true'},
		{street:"street2",postalCode:"code2",id:"5",isGreek:1},
		{street:"street3",postalCode:111343,id:null}
	]
}



projections form the user could be like the following:

Case 1
Plain simple projection.

ko.data.projections.projectVM(person,{
	firstName:'',
	lastName:''
})


would return

{
	firstName:"George",
	lastName:"Mavritsakis"
}



Case 2
Projection with a nested array schema.

ko.data.projections.projectVM(person,{
	firstName:'',
	lastName:'',
	addresses:[
		{street:"",postalCode:""}
	]
}) 


would return

{
	firstName:"George",
	lastName:"Mavritsakis",
	addresses:[
		{street:"street1",postalCode:"code1"},
		{street:"street2",postalCode:"code2"},
		{street:"street3",postalCode:"111343"}
	]
}



Case 3
Projection with a nested array schema and 'numeric' data type, resulting into translation of value (postacode).

ko.data.projections.projectVM(person,{
	firstName:'',
	lastName:'',
	addresses:[
		{street:"",postalCode:0}
	]
}) 


would return

{
	firstName:"George",
	lastName:"Mavritsakis",
	addresses:[
		{street:"street1",postalCode:NaN},
		{street:"street2",postalCode:NaN},
		{street:"street3",postalCode:111343}
	]
}



Case 4
Projection with a nested array schema and a custom function as the translator.

ko.data.projections.projectVM(person,{
	firstName:'',
	lastName:'',
	addresses:[
		{street:"",postalCode:0,isGreek:function(data,parent,root){ 
                    return data?'Yes':'No'; } 
                 }
	]
}) 


would return

{
	firstName:"George",
	lastName:"Mavritsakis",
	addresses:[
		{street:"street1",postalCode:NaN,isGreek='Yes'},
		{street:"street2",postalCode:NaN,isGreek='Yes'},
		{street:"street3",postalCode:111343,isGreek='No'}
	]
}




Case 5
Custom projection with a function.
This will match not existing properties

ko.data.projections.projectVM(person,{
	firstName:'',
	lastName:'',
        fullName:function(data,parent,root){
                return root.lastName+ ' ' +  root.firstName;
        }
}) 


would return

{
	firstName:"George",
	lastName:"Mavritsakis",
	fullName:"Mavritsakis George"	
}



Case 6
Projection with a nested array schema and an "directions" pair to be used for translation.
Directions can be either
[ defaultAndDataType,nullOrUndefVallue ]*
or
*[ defaultAndDataType,nullValue, undefValue ].
In any case if any translation results to undefined, then the system will return the value of defaultAndDataType.

ko.data.projections.projectVM(person,{
	firstName:'',
	lastName:'',
	addresses:[
		{street:"",isGreek:[false,true]}
	]
}) 


would return

{
	firstName:"George",
	lastName:"Mavritsakis",
	addresses:[
		{street:"street1",postalCode:NaN,isGreek=true},
		{street:"street2",postalCode:NaN,isGreek=true},
		{street:"street3",postalCode:111343,isGreek=true}
	]
}



Case 7
Data injections

ko.data.projections.projectVM(person,{
	firstName:'',
	lastName:'',
	additionInfo:{
		someData: 'whatever',
		otherData: function(data,parent,root){
			return root.firstName;
		}
	}
}) 


would return

{
	firstName:"George",
	lastName:"Mavritsakis",
	additionInfo:{
		someData: 'whatever',
		otherData:"George"
	}
}



Dependencies

In the current release the system needs jquery and knockout frameworks.

 

You can follow me on twitter: https://twitter.com/GMavritsakis

View George Mavritsakis's profile on LinkedIn

Last edited Oct 12, 2012 at 10:36 AM by gmavritsakis, version 17