JavaScript does not natively support the kind of loose coupling that the MVVM
pattern requires. Therefore, additional support must be introduced in
the form of a third-party library. Although there are many third-party
libraries available for JavaScript, few of them implement the MVVM
pattern. One library that does implement it successfully is Knockout, which you can find at http://knockoutjs.com.
Knockout is a JavaScript library that you can add to your apps
to implement the MVVM pattern. With Knockout, you can create ViewModel
components in JavaScript that effectively bind the server-side
SharePoint data sources to the webpages in your apps. The primary
capabilities of Knockout that makes the MVVM pattern possible are
declarative bindings and dependency tracking.
Declarative bindings make it possible for you to bind a ViewModel to HTML
elements in a webpage. Instead of writing HTML elements in your
JavaScript, the declarative bindings are defined within the app web
page. This approach removes the knowledge of the webpage structure from
the JavaScript code and creates the loose binding required by MVVM. Example 2 presents an example of declarative bindings in Knockout.
Example 2. Declarative bindings
<div id="resultsDiv" style="overflow: auto">
<table>
<thead>
<tr>
<th>Last Name</th>
<th>First Name</th>
<th>Phone</th>
</tr>
</thead>
<tbody id="resultsTable" data-bind="foreach: get_contacts()">
<tr>
<td data-bind="text: get_lname()"></td>
<td data-bind="text: get_fname()"></td>
<td data-bind="text: get_phone()"></td>
</tr>
</tbody>
</table>
</div>
In Example 2, the data-bind attribute binds a method from the ViewModel to HTML elements in the webpage. Note how the table body uses a foreach construct to iterate through a set of contacts returned from the get_contacts
method and build a table row for each contact. The last name, first
name, and phone number associated with each contact is then bound to a
table cell within a row.
When you develop a ViewModel, you start by creating a library to hold the data for binding. Example 3 shows a JavaScript library
that holds the data for an individual contact. Note how the properties
of the contact are exposed through public methods, and these methods
are the ones referenced in the associated HTML of Example 2.
Example 3. The contact data library
"use strict";
var Wingtip = window.Wingtip || {}
window.Wingtip.Contact = function (ln, fn, ph) {
//private members
var lname = 'undefined',
fname = 'undefined',
phone = 'undefined',
set_lname = function (v) { lname = v; },
get_lname = function () { return lname; },
set_fname = function (v) { fname = v; },
get_fname = function () { return fname; },
set_phone = function (v) { phone = v; },
get_phone = function () { return phone; };
//Constructor
lname = ln;
fname = fn;
phone = ph;
//public interface
return {
set_lname: set_lname,
get_lname: get_lname,
set_fname: set_fname,
get_fname: get_fname,
set_phone: set_phone,
get_phone: get_phone
};
}
The associated ViewModel performs the queries against the back-end
data source and populates the data objects. The ViewModel also provides
the public interface that is referenced in the data-bind attribute of the HTML for retrieving the collection of list items. Example 4 demonstrates a complete ViewModel that queries a contacts list and creates an array of contact information.
Example 4. The contacts ViewModel
"use strict";
var Wingtip = window.Wingtip || {}
Wingtip.ContactViewModel = function () {
//private members
var contacts = ko.observableArray(),
get_contacts = function () { return contacts; },
load = function () {
$.ajax(
{
url: _spPageContextInfo.webServerRelativeUrl +
"/_api/web/lists/getByTitle('Contacts')/items/" +
"?$select=Id,FirstName,Title,WorkPhone" +
"&$orderby=Title,FirstName",
type: "GET",
headers: {
"accept": "application/json;odata=verbose",
},
success: onSuccess,
error: onError
}
);
},
onSuccess = function (data) {
var results = data.d.results;
contacts.removeAll();
for (var i = 0; i < results.length; i++) {
contacts.push(
new Wingtip.Contact(
results[i].Title,
results[i].FirstName,
results[i].WorkPhone));
}
},
onError = function (err) {
alert(JSON.stringify(err));
};
//public interface
return {
load: load,
get_contacts: get_contacts
};
}();
The difference between Example 4 and Example 1 is significant. Example 1
relies on intimate knowledge of the user interface to create a table
and push it onto the webpage for display. The ViewModel in Example 4
simply creates an array of contacts. You can then easily bind this
array to various display forms such as a table or unordered list.
The most important aspect of the ViewModel is its use of the ko.observableArray type for handling the collection of contacts. The Knockout library provides the ko.observableArray
type specifically to implement dependency tracking. With dependency
tracking, automatic updating of the user interface occurs whenever data
elements in the array change. When your app initializes, it should load
the observable array and then bind the data. Knockout initializes the data binding when you call the applyBindings method. Example 5 illustrates how to initialize the data bindings with the ViewModel.
Example 5. Initializing data bindings
$(document).ready(function () {
Wingtip.ContactViewModel.load();
ko.applyBindings(Wingtip.ContactViewModel, $get("resultsTable"));
});