Hi Habr! In this article I will consider one of the options for building a client-server web application architecture from the point of view of data binding. This option does not claim to originality, but personally allowed me to significantly reduce development time, as well as optimize load time.
Problem
Suppose we have a large web interface that should display and allow the user to interact with several hundred elements of different types at the same time.
For each type of object we have our own class, and we naturally want to associate user actions in the interface with the methods of these classes.
In this case, we consider a simple example - the object has a name (the name property), and the object management interface is a text field where this name is entered. When the field changes, we want the object to change the property, and a new value is sent to the server (the SetName method).
Typically, a typical application initialization sequence looks like this:
- Initialize all objects
- Build DOM Interface Tree
- Get links to key interface elements (object container, object editing form, etc.)
- Initialize the interface with the current object property values
- Assign object methods as event handlers on interface elements
')
Head-on
The simplest implementation for a single object is as follows:
function InitDomForObject(object){
Obvious disadvantages of this implementation:
- Hard coupling of layout and JS code
- A huge amount of DOM build code and event handler assignments (for complex interfaces)
- With this approach, building a DOM will take too long for the browser, because we will call createElement, setAttribute and appendChild several times for each object, and these are rather “heavy” operations.
Templates
Faced with such difficulties, we immediately come to mind using templates and not generating DOM manually, because we are well aware that if we build the interface as a string with HTML code, it will be processed by the browser almost instantly.
We built HTML (for example, on the server side), and brought it to the browser, but we were faced with the fact that we would have to look for elements in a large DOM tree.
Due to the fact that we need to assign handlers to the elements - we cannot use lazy initialization, we still have to find absolutely all interface elements when loading the application.
Suppose all our objects of different types are numbered, and are collected in an array of Objects.
Now we have two ways:
Option A
Search for items by class by writing the object ID in the rel attribute.
An HTML representation of a single object will look like this:
<div class="container" rel="12345"> <input type="text" class="objectName" rel="12345" /> </div>
Then for each type of interface elements we assign approximately the following handler:
$(".objectName").change(function(){ var id = $(this).attr("rel");
And if for some reason we also want to keep the links to each element of the interface, we generally have to write a creepy loop over the entire array of objects: this is long and inconvenient.
Option B
Of course, we know that searching for items by id is much faster!
Since our id must be unique, we can use for example the following format "name_12345" ("role_id"):
<div id="container_12345" class="container"> <input type="text" id="name_12345" class="objectName" /> </div>
The assignment of handlers will look almost the same:
$(".objectName").change(function(){ var id = this.id.split("_")[1];
Since now all the elements can be found by ID, and the handlers are already assigned - we may well not collect all the links at once, but do it out of necessity (“lazily”) by implementing the GetElement method somewhere in the basic prototype of all our objects:
function GetElement(element_name){ if(!this.element_cache[element_name]) this.element_cache[element_name] = document.getElementById(element_name + '_' + this.id); return this.element_cache[element_name]; }
Search by ID is very fast, but the cache has never interfered with anyone. However, if you are going to remove items from the tree, please note that as long as there are links to them, the garbage collector will not get to them.
We will have only one problem: a large number of event handlers destination code, because for each type of interface element we will have to assign a separate handler for each event! The total number of appointments will be =
number of objects X number of elements X number of events .
Final decision
Recall the remarkable property of events in the DOM:
capturing and bubbling (
capturing and bubbling ):
because we can assign event handlers on the root element, because all the same, all events pass through it!
We could use the jQuery.live method for this purpose, and would come up with what is written above:
Option B , namely, a large number of handler assignment code.
Instead, we will write a small "router" for our events. We agree to start all the id elements with a special symbol to exclude elements for which no event handlers are needed. The router will try to forward the event to the object to which this element belongs, and call the appropriate method.
var Router={ EventTypes : ['click', 'change', 'dblclick', 'mouseover', 'mouseout', 'dragover', 'keypress', 'keyup', 'focusout', 'focusin'],
Example of use:
<input type="text" id="-Name_12345">
SomeObject.prototype = { … Name_blur : function(e){
Advantages of the solution:
- No need to assign many individual handlers, minimum code
- Each event is automatically sent to the desired object method, and the method is called in the desired context.
- You can add / remove interface elements from the layout at any time; you only need to implement the appropriate methods on your objects.
- The number of handler assignments is equal to the number of event types (and not the number of objects X, the number of elements X, the number of events )
Minuses:
- It is necessary in a special way and in large quantities to assign ID elements. (This only requires changing the patterns)
- At each event in each element, our EventHandler is called (it almost does not reduce performance, since we immediately discard unnecessary elements and also call stopPropagation)
- Object numbering should be end-to-end (you can divide the Objects array into several - one for each of the object types, or you can enter separate internal ordinal indexes, instead of using the same ID as on the server)
Other options
Fast decision
If our interface were standard and simple (i.e., used only standard controls), we would use the usual data binding method, for example, jQuery DataLink:
$("#container").link(object, { name : "objectName" } );
When the object property changes, the value of the text field changes and vice versa.
However, in reality, we often use non-standard interface elements and more complex dependencies than “One interface element to one property of an object”. Changing one field can affect several properties at once, and for different objects.
For example, if we have a user (
UserA ) with some rights, which belongs to a group, as well as an element in which you can select a group (
GroupA or
GroupB ).
Then changing the selection in this list will entail many other changes:
In the data:
- The property UserA.group will change.
- UserA object will be removed from GroupA.users array
- The UserB object will be removed from the GroupB.users array .
- Change array UserA.permissions
In the interface:
- The view of the list of user rights will change
- The counters showing the number of users in a group will change.
Etc.
Such complex dependencies cannot be easily resolved. Just in this case, the method described above is suitable.
Similar solution
A similar approach is applied to vKontakte: each event element is assigned through the appropriate attributes (onclick, onmouseover, etc.). Only the template is built on the client side, not the server.
However, event handling is not delegated to any object:
<div class="dialogs_row" id="im_dialog78974230" onclick="IM.selectDialog(78974230); return false;">
Instead, methods of global objects are called, which is not very good if, for example, the OOP approach is used in an application.
We could change this principle, still directing events to the right methods, but it would not look very nice:
<div class="dialogs_row" id="im_dialog78974230" onclick="Dialog.prototype.select.call(Objects[78974230], event); return false;">
Instead, we can adapt our function router to this approach:
<input type="text" id="Name_12345" onblur="return Route(event);">
function Router(event){ var route = event.target.id.split('_'); var elementRole = route[0]; var objectId = route[1]; var object = App.Objects[objectId];
This will save us from handling a large number of events for each "sneeze" of the user, but it will force us to describe the events in the template of each element and, moreover, the inline code.
Which of these evils is the least, and accordingly which of the methods to choose depends on the context and the specific task.