Using MVVM in JavaScript with Knockout

Get started using a powerful JavaScript framework that can help streamline UI coding in your web apps

Brian Mains

May 30, 2012

15 Min Read
shirtless man with bruised face and boxing gloves

Model-View-ViewModel (MVVM) is an architectural design pattern that has been popularized by Windows Presentation Foundation (WPF) and Silverlight. For more information on Knockout, see "Ease HTML5 Web Development with jQuery, Knockout, and Modernizr Libraries" and "Improve Your JavaScript Coding with Data-Oriented Programming." Simply put, the pieces of the pattern are these:

  • Model—the model data for the view to consume

  • View—the user interface (UI) that the user actually sees

  • ViewModel—a wrapper class around the model that provides additional functionality, including business logic

MVVM has also gained popularity in various JavaScript frameworks. MVVM's ability to link HTML elements to a data model makes it an attractive offering for managing complex UIs. Most applications have code that performs tasks, such as "if drop-down value is in X state, show another drop-down" or "if user enters too high or low of a numerical value, show a message asking the user to adjust the value." The intent of Knockout and other MVVM framework offerings is to move those conditional logic statements into the ViewModel and let the ViewModel be the sole source of the business logic. In this article, we'll explore some examples that will give you a feel for using Knockout in coding UIs for your web applications.

Getting Started with Knockout

Let's start by first examining a simple example of MVVM without the Knockout JavaScript framework. The sample page in Figure 1 represents a simple check-out screen.

Enter your zip for availability of this product:Product is available, enter quantity (max 5):The product is not available or you have too many products in your cart.This item has been added to the cart with a quantity of The code in Figure 1 is not overly complex, but you can see that we're getting into a lot of state management programming here. As the flow of the form changes state (e.g., valid to invalid zip code, valid zip code to valid quantity to confirmation), the amount of code increases to manage these interactions. For instance, if the user enters an invalid zip code, a rejection message is displayed to the user. If the entered zip code is valid, the user can proceed to enter the quantity and receive a confirmation.The Knockout MVVM implementation eliminates the need to write these sorts of explicit "if" statements and shifts their functionality into a common ViewModel. The UI reacts to changes in the state of the ViewModel (and vice versa) instead. Figure 2 contains a rewritten version of Figure 1's JavaScript, using the ViewModel to contain all the current states of the form. To do this, we construct a ViewModel using the class shown in Figure 2.function viewModel() {var self = this;this.quantity = ko.observable(0);this.zipCode = ko.observable("");this.shouldShowRejection = ko.computed(function() {return  (self.quantity() < 0 || self.quantity() >= 5) ||self.zipCode().length > 0 && zipsForAvailability.indexOf(self.zipCode()) == -1;});this.shouldShowConfirmation = ko.computed(function() {return self.quantity() >= 1 && self.quantity() <= 5 &&zipsForAvailability.indexOf(self.zipCode()) > -1;});this.shouldShowSignup = ko.computed(function() {return zipsForAvailability.indexOf(self.zipCode()) > -1;});};Notice how the model uses the "ko" (Knockout) object; this object creates bindings that manage the two-way relationship between the view and ViewModel. The bi-directional relationship happens through the use of observables, objects that track the state of the view and ViewModel. As data changes in either the view or ViewModel, the other is instantly notified of the change.In addition to observables, computed values create a derived value from observables or other values. Computed values work well for combining information and returning a different result. They also participate in the two-way binding, triggering an update when the inner observable that's used updates its value.With observables, Knockout is creating a function; to read or write from the quantity value, for instance, you invoke the quantity() function, as shown in the shouldShowConfirmation computed value. The quantity function can also take a parameter value, which then overwrites the value.At the very end of each model, the model is applied to the view by calling the ko.applyBindings method. This method takes a reference to the view and optionally, a second parameter reference to a parent element to apply the binding to.Binding to the UIThe Knockout framework applies bindings to the UI by using an HTML5 data attribute. This data-bind attribute identifies two key pieces of information. The first declaration is the binding to apply to the element. A binding essentially specifies an action to take on the element. Bindings may assign a value directly, add or remove Cascading Style Sheets (CSS) classes or styles, show or hide elements, manipulate the Document Object Model (DOM), or perform other types of actions. For instance, the CSS binding adds or removes a CSS class depending on a condition, which is supplied by the second parameter.This second parameter defines what to bind. It binds observables, computations, or functions defined on the ViewModel. The statement can be the name of the observable member or a conditional statement (which is supported for some bindings). Figure 3 shows several examples of different types of bindings (excluding the entire markup).In Figure 3, the view defines a data-bind attribute specifying how Knockout should apply itself to the element. For the first two text inputs, Knockout will apply the current value of the zipCode and quantity observables defined on the ViewModel. As the text inputs change their value and fire the LostFocus client event, the observable instantly receives the updated value, and vice versa. Notice that within the view, quantity is not a method call as we saw previously in Figure 2, but a reference to the member. Computed values are also referenced in the same way.You can make expressions more "expressive." For instance, some bindings support the traditional JavaScript evaluation statements. In such cases, we could, for example, define the data-bind declaration as "visible: quantity() > 10". Knockout can, in some cases, use a statement like this for evaluation of the capability. Otherwise, a computed value will always suffice.Form-Field BindingsKnockout provides a variety of useful bindings for form elements. In addition to the value binding just described, Knockout provides bindings for enabling and disabling elements. For example, to enable an element based on a ViewModel observable, the HTML element simply needs to apply the "enabled" or "disabled" keyword within the data-bind statement, and Knockout will enable or disable the content if the evaluated ViewModel property returns true. Otherwise, the reverse happens.Other similar bindings work this way; for example, the hasfocus binding can manipulate which control has the focus of the user's input. The hasfocus binding is great for changing focus when the values provided are empty or invalid and has a variety of other uses.Knockout also contains a few specialized bindings specific to certain control types. The first binding of this type is the checked binding, which works with controls supporting checked arguments (such as the CheckBox or RadioButton). This binding matches the value of the checked control and tries to match it with the value it is bound to in the ViewModel. The checked binding also works with arrays and will match multiple check boxes to a value in the array.The last specialized binding I will discuss is for select elements. A select element displays a list of items using a collection of option elements as its children. Knockout can preload and preselect the options list by using the options and selectedOptions bindings. These bindings use an array to populate the list. If the source data happens to be an object, which is a more complex binding, the optionsText and value bindings can be used to specify the value within the bound object to bind to.Control-Flow BindingsKnockout supports iterators and conditional statements using the same data-binding syntax as discussed in the previous section; Figure 4 shows an example usage. In Figure 4, the foreach binding iterates through each item in the array and supplies the item at the specified index to the context of the binding. In Figure 4, the parent div specifies the collection to iterate through. The children of the div (the table) represent the template for each item within the array; this means that this template is reused for every item in the list and that bindings should be specified relative to the item level.Notice the use of "with" in Figure 4. Similar to Visual Basic's implementation, the "with" statement saves us from referring to the same content over and over again. In referring to the product's information to bind to the UI, we could have referred to each property as "product.name" and "product.description". The "with" statement changes the current context of the binding, thus avoiding the need to type the word "product" multiple times.Additionally, the "if" binding controls whether or not to show the table based upon the value specified by the "isAvailable" property. If a product isn't available, a value of false prevents the table from appearing. Similarly to the visible binding, "if" and "ifnot" control whether or not something should appear, by actually adding or removing the content from the DOM.Event BindingsKnockout also supports setting up event bindings on elements. However, Knockout remains consistent in its event binding definitions, still utilizing the data-bind syntax as I've described. You can bind an element by using this statement: data-bind="click: handle". The standard approach to listening to events can certainly be used, but Knockout provides an added edge: Events fire callbacks defined in the ViewModel, which in turn may update an observable property, which then may update the UI as necessary.Event bindings support the click event and submit the event information by name directly. To attach to any other event, use the event binding, supplying a hash containing the names of the DOM event and their designated handler. This can be used to attach to events such as mouseover or mouseout. Figure 5 shows an example of attaching to events.Enter Text:SubmitNotice in Figure 5 how each event has two parameters. The second parameter contains the event information raised with the underlying event that's inherently available through JavaScript. The first parameter contains a reference to the current bound context within the data element. In this simple example, the data element refers to the current ViewModel. In the instances where a foreach looping container is used, the context will be the current item in the loop. The reference is essentially the equivalent of the $data reference. What is $data? Read on.Context and Scope ReferencesWhen a ViewModel or one of its members is bound to an element, the context of that element is mapped to the source. This can be important with foreach bindings, where the parent container is bound to the collection but the inner template is bound to the context of each item. Other bindings such as "with" can change the bound context to point to a nested or parent object, thus changing the binding's scope. If, for some reason, you need to access an observable on the ViewModel or from a parent context, there are a few references you can use.Within a child element, to access the direct parent context, use the $parent reference. In most cases, with simple bindings this will be a reference to the ViewModel. In other cases, such as with control-flow bindings, the $parent reference amounts to the parent object. To check whether an object exists in multiple parents, use the $parents reference, which is an array of the parent objects in the stack, or the $root object, which is a pointer to the ViewModel.Sometimes you might need to access the object within the current binding. The $data binding refers to this object and can be useful as a shortcut to the object, especially if the "this" pointer is pointing to something other than the current object/element. To illustrate this point, Figure 6 provides an example of using control containers that reference the parent ViewModel. The code in Figure 6 isn't practical; it is meant to help you understand scope.In the example in Figure 6, the $data reference referred to the current product object. This is because the "with" statement changed the scope of the container to the product. While $data refers to the width, $parent refers to the individual result, scoped at the foreach level. This allows us to refer to the isAvailable property directly. The $parents method provides the same result, returning the value of the isAvailable property. However, in this context, $parents also keeps drilling up the scope chain until it reaches the ViewModel. Here, it finds the reference to canPurchase and invokes it. Notice that this method defines one parameter; the parameter passed in is the object bound at the current scope (namely, the result).TemplatesIn our foreach example binding in Figure 6, the container element defining this binding contains child elements that represent the template for each item that's actually bound. Knockout extracts this template and uses it for each item bound to the list. An alternative to this templating scheme—which is the native templating approach that Knockout uses—is to leverage an external framework for templating, such as the jquery-tmpl plug-in or the Underscore or Prototype templating engines. A discussion of using an external framework is outside the scope of this article. Yet another templating approach and the one I'll focus on here is named templates. A named template looks like the code in Figure 7 and can be applied to an HTML element using the syntax shown in the figure.
Sign up for the ITPro Today newsletter
Stay on top of the IT universe with commentary, news analysis, how-to's, and tips delivered to your inbox daily.

You May Also Like