Loading...
Loading...
Marco Franssen

Knockout.js is a JavaScript library for building interfaces with the MVVM pattern. In this post, I'll show how its mapping plugin turns JSON data into observable view models. I've already covered some of Knockout's other handy features in a previous post:
http://marcofranssen.nl/knockout-that-cascading-dropdown/
If you'd like to brush up on Knockout first, visit Knockout's documentation.
Let's zoom in on the Knockout mapping plugin. It lets you easily map a JSON object into an observable JavaScript view model. Here's a short example of how you can use it when an Ajax request returns JSON.
var viewModel;
// result could look like this: "{ "personId": 1, "firstName": "Marco", "lastName": "Franssen", "age": 26, "webpage": "http://marcofranssen.nl", "twitter": "@marcofranssen" }"
$.ajax({
url: 'http://somewebpage.net/getPerson'
type: 'GET',
dataType: 'JSON',
success: function (result) {
var data = JSON.parse(result);
viewModel = ko.mapping.fromJS(data);
ko.applyBindings(viewModel, $('#person').get(0));
},
error: function (result) {
//handle the error, left for brevity
}
});We're making a request to a URL that returns a person as JSON. We then parse the JSON into a regular JavaScript object and map it into our view model. Finally, we bind the view model to the DOM nodes inside our person element, which could be a div. The plugin handles the observable properties we'd otherwise define by hand, as illustrated below.
var data = JSON.parse(result);
viewModel = {
personId: ko.observable(data.firstName),
firstName: ko.observable(data.firstName),
lastName: ko.observable(data.lastName),
age: ko.observable(data.age),
webpage: ko.observable(data.webpage),
twitter: ko.observable(data.twitter),
};The mapping plugin makes it easy to turn JSON data into an observable view model. But what if we want to skip some properties or copy them without wrapping them in an observable? We don't have to write the whole object by hand. We can configure a mapping object and pass it to ko.mapping.fromJS.
var mapping = {
ignore: ["twitter", "webpage"],
copy: ["age", "personId"],
lastName: {
create: function (options) {
return ko.observable(options.data.toUpperCase());
},
},
};
viewModel = ko.mapping.fromJS(data, mapping);The mapping above tells the plugin to ignore the twitter and webpage properties. It copies personId and age without wrapping them in observables and converts lastName to uppercase before putting it in an observable. Here's an illustration of defining a view model by hand.
viewModel = {
personId: data.personId,
firstName: ko.observable(data.firstNametoUpperCase()),
lastName: ko.observable(data.lastName),
age: data.age,
};You might be wondering why we'd skip observable wrapping for some properties. The main reason is that we know a property won't change, either through user input or an update from the server. A large number of observables takes more memory. For example, with a big collection of people in a table, you could skip observables for some properties to save memory and gain a small performance boost.
The mapping plugin can also help when you receive a collection of people from the server. Without it, you might create a view model and add the people to an observable collection like this.
var viewModel = {
persons: ko.observableArray(),
};
var persons = JSON.parse(jsonPersonArray);
for (person in persons) {
viewModel.persons.push(person);
}
//Or even better performance wise (persons observableArray gets updated only once)
viewModel.persons.push.apply(viewModel.persons, persons);In the example above, the person objects are plain JavaScript objects without observables. We can change that by creating a view model for each person. You might also want to check whether a person is already in the observable array. Imagine how much code that could add to the loop. Luckily, we have the mapping plugin.
First, I'll define a constructor for my person view models. We'll reuse the mapping from earlier and add a computed property by hand. You can add as many properties as you need to give your person objects more functionality.
function PersonViewModel(data) {
var personMapping = {
ignore: ["twitter", "webpage"],
copy: ["age"],
lastName: {
create: function (options) {
return ko.observable(options.data.toUpperCase());
},
},
};
ko.mapping.fromJS(data, personMapping, this);
this.fullName = ko.computed(function () {
return this.firstName() + " " + this.lastName();
}, this);
}I'll also create a constructor for the root view model. This encapsulates the mapping logic and lets me create multiple instances. Let's imagine a company view model containing employees. The server returns JSON with an array of people called employees, along with some other properties. We'll convert the company's name to uppercase and ignore its address and website properties.
function CompanyViewModel(data) {
var companyMapping = {
ignore: ["address", "website"],
name: {
create: function (options) {
return ko.observable(options.data.toUpperCase());
},
},
employees: {
key: "peronsId",
create: function (options) {
return new PersonViewModel(options.data);
},
},
};
ko.mapping.fromJS(data, companyMapping, this);
}Notice that we create a new person view model for each person in the employees array. PersonViewModel handles its own mapping in its constructor. We've also defined a key so Knockout can identify each person uniquely. Note that we haven't customized how personId is mapped in the person view model.
Properties not specified in the mapping object are wrapped in observables by default.
When we make a request, we can instantiate our view model like this.
var comany;
$.ajax({
url: "http://companyindex.org/getcompanydata/4327/",
type: "GET",
dataType: "JSON",
success: function (result) {
var data = JSON.parse(result);
company = new CompanyViewModel(data);
ko.applyBindings(company, $("#company").get(0));
},
error: function (result) {
//left for brevity
},
});We call a URL that returns company information as JSON, create a CompanyViewModel, and apply the bindings. All the mapping logic lives in the constructors. See this jsFiddle for a complete example.
Thanks again for reading. Please leave a comment and share this article with your friends and colleagues.
Marco Franssen
Start a .NET continuous integration build with an MSBuild script and a Windows batch file, then choose Debug or Release builds from the command line.
Marco Franssen
A personal look back at my 2012 blog posts, favorite technical articles and starred GitHub projects, covering JavaScript, .NET, CQRS and Windows.
Marco Franssen
Use Windows Phone 8 accent colors in your apps with a C# helper for hexadecimal color values and a SolidColorBrush property for MVVM binding.
Marco Franssen
Unblock downloaded files in Windows with PowerShell, preview changes with WhatIf, and use Recurse to include files in subfolders.