📜 ⬆️ ⬇️

KnockoutJS usage example

Good day, habrasoobschestvo! The article will consider the process of creating a web-page editing list of users. Ready example can be taken here . The article is not a review, but close to real hostilities, therefore I strongly recommend that you familiarize yourself with a simple example . I decided to try using the above framework. The need for this step is due to the fact that another bicycle was invented over and over again to describe the rich logic on the client side. It looked like this:
(function($) { var initListeners = function() { /*    , , click, keypress, focus  ..*/ } var updateListeners = function() { /*   ,      */ } var createUser = function() { /*  Ajax          */ } var updateUser = function() { /*  Ajax          */ } var parseResponse = function() { /* JSON-     */ } initListeners(); })(jQuery) 
This approach may seem like a good option for javascript-code with a length of 100 lines. However, any modification of it can be costly when the code grows into 200 or more lines. The main disadvantages in my opinion are: the presence of routine actions (event management, adding methods to JSON objects), a poorly formalized description of the pageController structure, strong html and javascript connectivity. In fact, often such code rolls into the jumble of nested callbacks and the elusive logic of choosing the level of description of a new function. It was decided to use the framework to give the code structure. The choice fell on the MVVM framework KnockoutJS and here's why:

We formulate the problem

Suppose you need to write part of the application responsible for editing the list of users. Each user is known to be represented by the following set of fields:
 { Id : "", Surname: "", FirstName: "", PatronymicName: "", Login: "", EMail: "" } 
It is also known that the server can perform such actions on users as:
  1. Send current user list to customer
  2. Delete user by user ID
  3. Add / update user record
All these actions can be described by a stub, schematically presented as follows:
 function DataGate() { var modelStub = { users: [...] }; return { Load : function(callback) { callback(modelStub); }, DeleteUser : function(callback, id) { callback(true); }, SaveOrUpdateUser : function(callback, user) { callback(user); } } } var gate = new DataGate(); 
Let's define the page display requirements (I will call the page elements as the user represents them, and not how they are implemented in the html code):It is reasonable to edit the user in a separate dialog box (since not all his fields are necessary in the list, and some fields are composite, the direct editing of which is a hit song, which, however, has a solution ). It should be noted that directly changing the user’s fields inside the dialog box will lead to a change in its presentation within the list. This is undesirable because you must first save the user, and if everything went well, then close the dialog and apply the changes on the list. To organize this behavior, it is reasonable to divide the page's ViewModel into two parts. Then the ViewModel of the list will be superimposed on the html-code immediately after receiving the list of users, and the ViewModel of editing the user will be superimposed on the parent div of the dialog at the moment of its display.

Implementing a list of users

Javascript code

Define the ViewModel for the list of users:
 var viewModel = { selectedUser : ko.observable(null), //      deleteSelectedUser : function() { /*             users */ }, createUser : function () { /*      */ } } 
Wait, where are the users ? ViewModel will be expanded by the utility , which will generate observer objects from JSON strings or from POJO objects. This is how the "casting" of simple objects from the server to objects with methods and observer properties ready for use with KNockoutJS is organized. Take a close look at the excerpt from the DataGate code:
 { users: [...] } 
Yes, the viewModel after performing the mapping procedure will have a users field, however this will not be an ordinary array, but an array observer . Moreover, the mapping is performed deeply, which means that the entire object graph from the server will be reduced to the same observer object graph. It is worth noting that the process of such mapping can be flexibly controlled:
  1. Define a constructor function for each object in the graph
  2. If the object is an array of other objects, then determine among them the key field
This setting can be done as follows:
 var mapping = { users: { key: function(data) { return ko.utils.unwrapObservable(data.Id); }, create: function(options) { return new userMapping(options.data); } } } 
As you can see, the Id field is used as the key, and the constructor function is used to build the user object:
 var userMapping = function(user) { ko.mapping.fromJS(user, {}, this); /*    user    this */ this.FIO = ko.dependentObservable(function() { return this.Surname() + " " + this.FirstName() + " " + this.PatronymicName(); }, this); /*        this  ,      user */ var _self = this; this.select = function() { viewModel.selectedUser(_self); } /*    */ this.edit = (function() { /*      */ })(); /*    ,              */ } 
Bind the viewModel to the page:
 $(function() { gate.Load(function(loadedModel) { ko.mapping.fromJS(loadedModel, mapping, viewModel); ko.applyBindings(viewModel); }); }); 

HTML code

All code will be located inside the <body> tag:
 <h2> </h2> <div data-bind="jqueryui : 'buttonset'"> <button data-bind="click: createUser"></button> <button data-bind="click: deleteSelectedUser, enable: selectedUser() !== null"></button> </div> <div id="UserListTable"> <div> <table class="list"> <thead> <tr> <th></th> <th></th> </tr> </thead> <tbody data-bind="template: {name: 'editUsersRow', foreach: users, templateOptions: { current: selectedUser } }"> </tbody> </table> </div> </div> <script type="text/html" id="editUsersRow"> <tr data-bind="attr: { 'data-id': Id }, click: select, css: { selected: $data == $item.current() }"> <td><a href="#" data-bind="text: FIO, click: edit"></a></td> <td data-bind="text: Login"></td> </tr> </script> 
Some notes on the code:As you can see, connectivity is minimal, html-code is compact, dynamic and consistent. In principle, you can make ko-plugins for such things as dynamic edit / delete / insert tables, but there is already a similar concept.

Implementing user create / edit dialog

Javascript code

As mentioned above, for an adequate interface behavior it is necessary to implement a separate ViewModel dialog (preferably universal for both creating and editing). However, the mechanism for displaying the editing dialog can be represented by the following sequence of actions:
  1. Get a POJO user object from the selected one (this operation can also be performed using mapping ) or create an empty POJO user object (if you want not to edit it, but create a new one)
  2. Build HTML-code on the dialogue body template.
  3. Overlay the jQuery UI dialog to the built template.
  4. If the dialog is correctly displayed, bind the ViewModel with the dialog as the root element of the binding.
Code demonstrating this behavior, as well as a small workaround:
 function buildEditUserDlg(model, closeCallback){ return $("#editUserDlg").tmpl().dialog({ title: model.FIO()+" ", /*        ,        . */ width: 400, create: function(e) { var _self = this; ko.applyBindings(model, e.target); /*   ViewModel   */ model.FIO.subscribe(function(newValue) { /*       */ $(_self).dialog("option", "title", newValue+" "); }); model.isOperationComplete.subscribe(function(newValue){ /*   ViewModel   ,    ,           */ if (newValue === true) $(_self).dialog("close"); }) }, close: function(e) { /*     ,   . */ $(this).dialog("destroy").remove(); closeCallback(); }, buttons: { "" : function() { model.save(); }, "": function() { $(this).dialog("close"); } } }); } 
It remains to describe the code for opening the dialog for editing (the code for opening for creation is a special case). To do this, go back to the user mapping code:
 this.edit = (function() { var currentDialog = null; return function() { if (currentDialog != null) { return; } var dialogModel = new userEditDialogMapping(ko.mapping.toJS(_self)); currentDialog = buildEditUserDlg(dialogModel, function() {currentDialog = null}); }; })(); 
I note that the presence of a dialogue closing kolbek is a necessary measure, which would be nice to get rid of, but I could not make it beautifully.

HTML code

 <script type="text/html" id="editUserDlg"> <div> <table> <tbody> <tr> <th><label for="surname">:</label></th> <td><input maxlength="20" name="surname" data-bind="value: Surname" type="text" value=""></td> </tr> <tr> <th class="property"><label for="firstname">:</label></th> <td><input maxlength="20" data-bind="value: FirstName" name="firstname" type="text"></td> </tr> <tr> <th class="property"><label for="patronymicname">:</label></th> <td><input maxlength="20" name="patronymicname" data-bind="value: PatronymicName" type="text" value=""></td> </tr> <tr> <th class="property"><label for="email">E-Mail:</label></th> <td><input maxlength="30" name="email" data-bind="value: EMail" type="text" value=""></td> </tr> <tr> <th class="property"><label for="login">:</label></th> <td><input maxlength="20" name="login" data-bind="value: Login" type="text" value=""></td> </tr> </tbody> </table> </div> </script> 
That's all. The client application is ready! PS What is the best way to highlight HTML templates?

')

Source: https://habr.com/ru/post/121926/


All Articles