MVVM - The KnockoutJS Redemption

What is this article for?
 
Everyone out there is talking about AngularJS or ReactJS but they have forgotten about KnockoutJS and how it can help you. So, this is the KnokoutJS redemption!
 
 
 
KnockoutJS is JavaScript plugin with a Model-View-View-Model (MVVM) pattern. When your data model’s state changes, your UI updates automatically. Like AngularJS, it provides a full control for a view and the model. 
 
What is the goal?
  1. Use KnockoutJS to handle the view;
  2. Write Javascript code in Revealing Module Pattern;
  3. Create a feature to insert, edit, and delete data;
  4. Demonstrate how KnockoutJS works using mapping, computed variables, and computed methods. 
What is the demo application?
 
I have a page that contains a list of people. When I click a person, I can delete or edit that person. I also want to add a new person. Very simple!
 
 
 
What do we need for this article?
  1. KnockoutJS JavaScript plugin;
  2. KnockoutJS JavaScript mapping plugin;
  3. jQuery JavaScript plugin;
  4. Our person's viewmodel in JavaScript;
  5. An index.html for our simple view; 
Index.html 
  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.     <title>MVVM: The KnockoutJS redemption</title>  
  5.     <meta charset="utf-8" />  
  6.     <script src="Scripts/jquery-1.10.2.min.js"></script>  
  7.     <script src="Scripts/knockout-3.2.0.js"></script>  
  8.     <script src="Scripts/knockout.mapping-latest.js"></script>  
  9.     <script src="Scripts/view-models/person.js?vswfsd"></script>  
  10. </head>  
  11. <body>  
  12. <div id="person">  
  13. <table border="1">  
  14. <thead>  
  15. <tr>  
  16. <th>#</th>  
  17. <th>Name</th>  
  18. <th>Gender</th>  
  19. <th></th>  
  20. </tr>  
  21. </thead>  
  22. <tbody data-bind="foreach: $root.list">  
  23. <tr>  
  24. <td><span data-bind="html: $index() + 1"></span></td>  
  25. <td>  
  26.                         <span data-bind="visible: !$data.IsEditing(), html: $data.Name"></span>  
  27.                         <input type="text" data-bind="visible: $data.IsEditing, value: $data.Name" /></td>  
  28. <td>  
  29.                         <span data-bind="visible: !$data.IsEditing(),html: $root.getGenderName($data)"></span>  
  30.                         <select data-bind="visible: $data.IsEditing, options: $root.genders, optionsValue: 'Id', optionsText: 'Name', value: $data.Gender" /></td>  
  31. <td>  
  32.                         <input type="button" data-bind="visible: !$data.IsEditing(), click: $root.onEdit, enable: !$root.isEditing()" value="edit" />  
  33.                         <input type="button" data-bind="visible: $data.IsEditing, click: $root.onSave" value="save" />  
  34.                         <input type="button" data-bind="click: $root.onDelete, enable: $data.IsEditing() || !$root.isEditing()" value="delete" /></td>  
  35. </tr>  
  36. </tbody>  
  37. </table>  
  38. <input type="button" data-bind="click: $root.onInsert, enable: !$root.isEditing()" value="insert" /></div>  
  39. <script type="text/javascript">  
  40.         $(document).ready(function () {  
  41.             // initial data  
  42.             personViewModel.init({ List: [{ Name: 'Jack', Gender: 0, IsEditing: false }, { Name: 'Charlie', Gender: 1, IsEditing: false }, { Name: 'Hugo', Gender: 0, IsEditing: false }] });  
  43.         });  
  44.     </script>  
  45. </body>  
  46. </html>  
It is a simple html with tables and other elements. The KnockoutJS initialization will be at,
  1. personViewModel.init({ List: [{ Name: 'Jack', Gender: 0, IsEditing: false }, { Name: 'Charlie', Gender: 1, IsEditing: false }, { Name: 'Hugo', Gender: 0, IsEditing: false }] });  
All KnockoutJS MVVM are defined in “data-bind” attributes on elements, for instance,
  1. <select data-bind="visible: $data.IsEditing, options: $root.genders, optionsValue: 'Id', optionsText: 'Name', value: $data.Gender" />  
It means, KnockoutJS will fill that selected element with options and those options will come from “$root.genders” array property. Also, select element will be visible only if “$data.IsEditing” property of a person is true.
 
In KnockoutJS, we use “$root” to elicit some method or property at root level of ViewModel. We use “$data” when a property or a method inside an item is in a foreach loop.
 
The JavaScript “personViewModel” in Revealing Module Pattern:
 
Scripts/view-models/person.js 
  1. /// <reference path="../knockout-3.2.0.js" />  
  2. /// <reference path="../knockout.mapping-latest.js" />  
  3. /// <reference path="../jquery-1.10.2.js" />  
  4.    
  5. var personViewModel = function () {  
  6.    
  7.     var _vm = null,  
  8.    
  9.     map = function (obj) {  
  10.         return ko.mapping.fromJS(obj);  
  11.     },  
  12.    
  13.     createComputed = function () {  
  14.    
  15.         _vm.isEditing = ko.computed(function () {  
  16.             return $.grep(_vm.list(), function (n) {  
  17.                 return n.IsEditing() === true;  
  18.             }).length > 0;  
  19.         }, _vm);  
  20.    
  21.     },  
  22.    
  23.     init = function (model) {  
  24.         _vm = {  
  25.             list: map(model.List),  
  26.             genders: [  
  27.                 { Id: 0, Name: 'Select...' },  
  28.                 { Id: 1, Name: 'Masc' },  
  29.                 { Id: 2, Name: 'Fem' }  
  30.             ],  
  31.             test: ko.observable('initial string value'),  
  32.             onEdit: function (person) {  
  33.                 person.IsEditing(true);  
  34.             },  
  35.             onSave: function (person) {  
  36.                 person.IsEditing(false);  
  37.             },  
  38.             onDelete: function (person) {  
  39.                 if (confirm('Are you sure?')) {  
  40.                     var index = _vm.list.indexOf(person);  
  41.                     _vm.list.splice(index, 1);  
  42.                 }  
  43.             },  
  44.             onInsert: function () {  
  45.                 _vm.list.push(map({ Name: 'new person', Gender: 0, IsEditing: true }));  
  46.             },  
  47.             getGenderName: function (person) {  
  48.                 if (person.Gender() === 0) {  
  49.                     return '-';  
  50.                 }  
  51.    
  52.                 return $.grep(_vm.genders, function (n) {  
  53.                     return n.Id === person.Gender();  
  54.                 })[0].Name;  
  55.             }  
  56.         };  
  57.    
  58.         createComputed();  
  59.    
  60.         var ctx = $('#person').get(0);  
  61.         ko.applyBindings(_vm, ctx);  
  62.     }  
  63.    
  64.     return {  
  65.         init: init  
  66.     }  
  67.    
  68. }();  
The “personViewModel” reveals just one method: the “init” method. You can see there are two other methods: “createComputed” and “map“. They are NOT exposed in the Revealing Module Pattern as they are private.

For that example, KnockoutJS will handle MVVM inside a DIV with
id=”person” and all observable properties inside “_vm” variable. The configuration is,
  1. var ctx = $('#person').get(0);  
  2. ko.applyBindings(_vm, ctx);   
All methods and properties inside “_vm” variable are called “ROOT level” that you can use “$root” syntax in the View.
 
It means, all the changes in View will propagate to the JavaScript and all the changes made in JavaScript will propagate to the View. If something is outside the “#person” DIV, the KnockoutJS will not handle it.
 
Be aware! Only the “genders” array in “_vm” is not handled by KnockoutJS. It means, if you try to insert a new gender or change any existing gender, KnockoutJS will not update the View. To configure KnockoutJS to handle an object array, we will need to use the KnockoutJS mapping plugin. I used that for the list of people “_vm.list“.
 
If you need to GET a property value handled by Knockout you will need to call with brackets.
  1. var value = person.IsEditing();  
If you need to SET a value for a property handled by KnockoutJS you will need to set it in brackets.
  1. person.IsEditing(true);   
I hope it helps! Good luck!
 
Download full source code


Similar Articles