Data binding with modern JavaScript frameworks

Due to the complexity that comes with data binding design patterns, there are some standalone JavaScript libraries, such as Rivets.js and Knockout.js, that can provide it for you. Many full-fledged JavaScript frameworks also include their own implementations of data binding as a core feature.

Data binding with AngularJS

AngularJS, which is maintained by Google, is one of the most popular modern JavaScript frameworks. As discussed in Chapter 2Model-View-Whatever it is a self-avowed MVW framework. In addition to its MVW architectural pattern implementation, it includes a powerful data binding design pattern, which is often its most touted feature.

One-way data binding with AngularJS

One-way data binding with AngularJS is achieved when an expression in the View is populated by a value from the Model associated with the Controller for that View. Consider the following Controller and Model data:

var myApp = angular.module('myApp', []); 
myApp.controller('UserController', function UserController($scope) { 
    $scope.user = { 
        firstName: 'Peebo', 
        lastName: 'Sanderson' 
    }; 
}); 

The user Model that is defined on the scope for this Controller can be represented in the View with the following template markup:

<body ng-app="myApp"> 
    <div ng-controller="UserController"> 
        <p> 
            <strong>First Name:</strong> {{user.firstName}}<br> 
            <strong>Last Name:</strong> {{user.lastName}} 
        </p> 
    </div> 
</body> 

Just like with many other JavaScript templating engines, the double curly brace syntax is used to represent expressions to be evaluated in an AngularJS template. Additionally, AngularJS allows for the use of the ng-bind attribute on empty HTML elements to be used in place of the double curly brace syntax for expressions:

<body ng-app="myApp"> 
    <div ng-controller="UserController"> 
        <p> 
            <strong>First Name:</strong> 
            <span ng-bind="user.firstName"></span><br> 
            <strong>Last Name:</strong> 
            <span ng-bind="user.lastName"></span> 
        </p> 
    </div> 
</body> 

This syntax is more verbose, but may be preferable to some. In either case, changes to the Model properties will be automatically updated in the View where those properties are bound by their respective template expressions. In this way, AngularJS provides the underlying DOM manipulation layer that wires Model changes to be updated in the View without any further code being necessary.

Two-way data binding with AngularJS

Two-way data binding with AngularJS is achieved when an editable value in the View, such as a text input, is assigned to a property on the Model for the current Controller scope. When the value for that property is changed by the user, the Model will be updated automatically, and that change will be propagated back to the View for any expression that is bound to that Model property.

Using the Controller and Model from the previous example, consider the following template markup:

<body ng-app="myApp"> 
    <div ng-controller="UserController"> 
        <p> 
            <strong>First Name:</strong> {{user.firstName}}<br> 
            <strong>Last Name:</strong> {{user.lastName}} 
        </p> 
        <p> 
            <label> 
                <input type="text" ng-model="user.firstName"> 
            </label><br> 
            <label> 
                <input type="text" ng-model="user.lastName"> 
            </label> 
        </p> 
    </div> 
</body> 

The text inputs are given the ng-model attribute to assign a Model property as the value when the View is initially loaded. When the user changes the value for either of these inputs, the $scope.user Model will be updated, and the change will then be reflected in the paragraph block above the inputs where the same properties are bound to the DOM by their respective expressions. This round-trip from a change in the View to the Model and back to the View again is a simple example of two-way data binding.

Dirty checking with AngularJS

AngularJS uses a method of polling for changes to find differences between the Model and the View, and this method is referred to as dirty checking. This checking is done on a defined interval, which is referred to as the digest cycle. For each digest cycle, special methods called watches are registered with listeners by the scope to watch for changes to bound expressions that are passed to them:

$scope.$watch(watchExpression, listener); 
 

As explained in Chapter 2, Model-View-Whatever the scope is a JavaScript object that defines the Model context for variable expressions in the View. The watches compare bound Model expressions with their previous values and if any of them are found to be dirty, or different, the listener callbacks are executed and the changes are then synced to the View.

AngularJS allows dirty checking to be performed at multiple levels of depth for an object, depending on your needs. There are three types of watch provided for this, with three respective depths. These levels provide for flexible data binding features, but with more depth comes more performance concerns.

Dirty checking by reference

The standard method of dirty checking in AngularJS watches for the entire value of a bound expression to change to a new value. This is referred to as dirty checking by reference. If the expression represents an object or an array, and only changes to its properties or members are made, the change will not be detected. This is the lowest depth of dirty checking, and thereby the most performant.

As an example, consider a user object with multiple properties is applied to the scope:

$scope.user = { 
    firstName: 'Peebo', 
    lastName: 'Sanderson', 
    age: 54 
}; 
 

Now a watch expression can be bound by reference to one of the object's properties:

$scope.$watch('user.firstName', listener); 
$scope.user.firstName = 'Udis'; 

Since user.firstName has changed, this will be picked up in the subsequent digest cycle and the listener function will be triggered. Now consider instead that we watch the user object itself:

$scope.$watch('user', listener); 
$scope.user.lastName = 'Petroyka'; 
// The entire value of $scope.user has not changed 
 

In this case, nothing is picked up by the watch after user.lastName is changed. This is because the watch is looking for the user object itself to change - not one of its individual properties:

$scope.user = { 
    firstName: 'Udis', 
    lastName: 'Petroyka, 
    age: 82 
}; 
// The entire value of $scope.user has changed 
 

If you were to instead replace the entire user object itself, the watch would find the value to be dirty and would then invoke the listener during the next digest cycle.

Dirty checking by collection contents

If you need to watch for shallow changes to an object or an array, AngularJS provides another method for watching called $watchCollection. In this context, shallow means that the watch will only respond to changes at the first level of the object or array - deep property changes, or those of nested objects or arrays, will not be detected. AngularJS calls this dirty checking by collection contents:

$scope.$watchCollection(obj, listener); 
 

In this case, changing a property of the user object from the previous example would be picked up by the watch and trigger the listener:

$scope.$watchCollection('user', listener); 
$scope.user.firstName = 'Jarmond'; 
// A property of the object has changed 
 

Dirty checking by collection contents is not as performant as checking by reference because a copy of the watched object or array must be kept in memory.

Dirty checking by value

AngularJS also allows you to watch for changes on any nested data within an object or array. This is referred to as dirty checking by value:

$scope.$watch(watchExpression, listener, true); 
 

You can implement this method of watching using the $watch method, just as you would with checking by reference, but with an added third parameter set to true. This parameter tells the watch whether you want to check for object equality or not, and it defaults to false. When the watch checks for equality by reference, it performs a simple !== conditional. When the third parameter of $watch is set to true, however, it uses the internal angular.equals method for a deep comparison.

The angular.equals method can be used to compare any two values, and it supports value types, regular expressions, objects, and arrays. If a property being compared is a function or its name begins with $, it will be ignored. The reason for ignoring functions is obvious, and as for the $ prefix, it is likely done to avoid AngularJS internal functionality from being overwritten.

Dirty checking by value is the most comprehensive form of data binding in AngularJS, but it is also the least performant. This is because a full copy of any complex object or array being compared must be held in memory, as is the case with dirty checking by collection contents, but additionally, a deep traversal of the entire object or array must be performed on each digest cycle. To maintain memory efficiency in your application, care should be taken when using this type of data binding with AngularJS.

When to use dirty checking for data binding

The dirty checking approach to data binding has its pros and cons. AngularJS assures us that memory is not a concern as long as you are not doing several thousand bindings in a single view. A downside is, however, that changes to the Model will not always show up in real time due to the latency of the digest cycle. If you are designing an application in which you would like to display true real-time, two-way data binding, then AngularJS may not be the solution for you.

Data binding with Ember.js

Ember.js is a popular open source JavaScript framework for building web applications. It is similar to AngularJS in its provided features, but it takes quite a different approach to data binding.

Ember.js runs an internal loop, similar to the digest cycle in AngularJS, called the run loop. It does not use dirty checking on bound Model data, but it maintains the run loop for other internal functionality, such as scheduling work queues to be performed in a particular order. The main reason behind scheduling operations within the run loop is to provide memory management and optimize the efficiency of the framework.

Ember.js uses property accessors to provide data binding, which means it uses direct object properties to get and set bound Model values. With this mechanism in place, it can forgo dirty checking to employ data binding.

Computed properties

Ember.js uses computed properties via object property accessors internally for setting and getting values. This means that properties are defined as functions that perform some type of manipulation to produce the final values that are returned. To do this, the native JavaScript object type is extended with the internal Ember.Object.extend method, and computed properties are returned using the Ember.computed method:

var User = Ember.Object.extend({ 
    firstName: null, 
    lastName: null, 
    fullName: Ember.computed('firstName', 'lastName', function() { 
        return `${this.get('firstName')} ${this.get('lastName')}`; 
    }) 
}); 

For this extended User object, the firstName and lastName properties are static, but the fullName property is computed with the 'firstName' and 'lastName' strings passed to it as parameters. This tells the computed method that those properties of the extended object are to be used in computing the returned value for fullName.

Now, to access the value that is returned by fullName, a new User object must first be created with the static firstName and lastName properties defined:

var currentUser = User.create({ 
    firstName: 'Chappy', 
    lastName: 'Scrumdinger' 
}); 

Once a currentUser object is created with a given firstName and lastName value, the fullName property can be computed and returned:

currentUser.get('fullName'); // returns "Chappy Scrumdinger" 

This convention of extending objects is a bit verbose, but it allows Ember.js to handle the computed properties internally and track bound objects while also normalizing JavaScript inconsistencies across various user agents, or browsers.

One-way data binding with Ember.js

Ember.js uses computed properties in its data binding implementation, which means that direct property accessors can be used and no dirty checking is necessary. For one-way bindings, you can get the property for an object, but you cannot set it:

var User = Ember.Object.create({ 
    firstName: null, 
    lastName: null, 
    nickName: Ember.computed.oneWay('firstName') 
}); 
 

In this example, the Ember.computed.oneWay method is used to apply a one-way binding for the nickName property as an alias of the firstName property:

var currentUser = User.create({ 
    firstName: 'Peebo', 
    lastName: 'Sanderson' 
}); 

When a new User object is created, the nickName property for it can then be accessed:

currentUser.get('nickName'); // returns "Peebo" 

Since this is only a one-way binding, however, the nickName property cannot be used to set the aliased firstName property:

currentUser.set('nickName', 'Chappy'); 
currentUser.get('firstName'); // returns "Peebo" 

Often, you may only need to return bound values in an application and not implicitly set them from the View. Wither Ember.js, the Ember.computed.oneWay method can be used for this purpose and will save you additional performance concerns.

Two-way data binding with Ember.js

Two-way data binding is also available with Ember.js via computed properties. This uses an alias paradigm as well; however, a computed two-way alias allows for both getting and setting an aliased property:

var User = Ember.Object.extend({ 
    firstName: null, 
    lastName: null, 
    nickName: Ember.computed.alias('firstName') 
}); 

In this instance, we are using the Ember.computed.alias method to employ two-way data binding for the aliased firstName property via the computed nickName property:

var currentUser = User.create({ 
    firstName: 'Udis', 
    lastName: 'Petroyka' 
}); 

When a new User object is created now, the nickName property can be accessed to both set and get the aliased firstName property:

currentUser.get('nickName'); // returns "Udis" 
currentUser.set('nickName', 'Peebo'); 
currentUser.get('firstName'); // returns "Peebo" 

Now, with two-way data binding, View synchronization comes into play. One thing to note about Ember.js in this scenario is that, although it does not use dirty checking, it will not immediately update values bound to the Model after they are changed. Property accessors are indeed used to aggregate changes to bound data, but they are not synchronized until the next run loop, just as with AngularJS and its digest cycle. In this respect, you could surmise that data binding with AngularJS versus Ember.js is really no different, and neither framework provides any benefit over the other in this regard.

Keep in mind that the internal looping mechanisms implemented in these frameworks are designed around performance optimization. The difference in this case is that AngularJS uses its digest cycle to check for changes to bound values, in addition to its other internal operations, while Ember.js is always aware of changes to its bound values and only uses its run loop to synchronize them.

It is likely that each of these frameworks provides certain advantages over the other, depending upon what type of application you are building. When choosing a framework to build an application, it is always important to understand these internal mechanisms so that you can consider how they may affect performance in your particular use case.

Data binding with Rivets.js

Sometimes it is desirable to build an SPA with smaller, more modular libraries that provide you with specific functionality, rather than using a full-fledged frontend framework such as AngularJS or Ember.js. This could be because you are building a simple application that does not necessitate the complexity of an MVW architectural pattern, or you may just not want to be constrained by the conventions of a framework.

Rivets.js is a lightweight library that is primarily designed around data binding, and although it does provide some additional features, it makes very few assumptions about your application architecture. In this respect, it is a good choice if you are only looking to add a data binding layer to a modularized application.

One-way data binding with Rivets.js

Rivets.js uses an internal construct called a binder to define how the DOM should be updated in response to a change in a bound property's value. The library comes with a variety of built-in binders, but also allows you to define your own custom binders.

One-way binders in Rivets.js update the DOM when a property on a bound Model changes. As you would expect in a one-way scenario, updating the View will not update the Model.

Consider the following object:

var dog = { 
    name: 'Belladonna', 
    favoriteThing: 'Snacks!' 
}; 

Using the binder syntax of Rivets.js, these properties can be bound to the View as follows:

<h1 rv-text="dog.name"></h1> 
<p> 
    My favorite thing is:  
    <span rv-text="dog.favoriteThing"></span> 
</p> 

Rivets.js uses the rv- custom attribute prefix on HTML elements to define behaviors for different types of binders. The rv-text attribute is a built-in binder that inserts a bound value directly into the DOM, just as any JavaScript templating engine might do. To that point, there is also an expression interpolation syntax that uses single curly braces:

<h1>{ dog.name }</h1> 
<p>My favorite thing is: { dog.favoriteThing }</p> 

With either of these examples, the View would render the following HTML:

<h1>Belladonna</h1> 
<p>My favorite thing is: Snacks!</p 

Changing any properties on the bound Model would also update the View:

dog.name = 'Zoe'; // binder in View is updated 
dog.favoriteThing = 'Barking!'; // binder in View is updated 
 

The rendered HTML in the View would then reflect these changes:

<h1>Zoe</h1> 
<p>My favorite thing is: Barking!</p> 
 

Defining your own one-way binder

If none of the many predefined binders in Rivets.js fits your needs, you can always define your own:

rivets.binders.size = function(el, val) { 
    el.style.fontSize = val; 
}; 

In this example, we have created a binder called size, which can be used to dynamically change the CSS font-size property for an element based on a Model value:

var dog = { 
    name: 'Belladonna', 
    favoriteThing: 'Snacks!', 
    size: '2rem' 
}; 

The custom binder can then be used in the View as follows:

<h1>{ dog.name }</h1> 
<p> 
    My favorite thing is: 
    <span rv-size="dog.size">{ dog.favoriteThing }</span> 
</p> 

This would render the View with the dog.favoriteThing value displayed at twice the font-size of the body text, as defined in the bound dog Model.

Two-way data binding with Rivets.js

Two-way binders in Rivets.js behave just as one-way binders do when a Model is updated by synchronizing the bound values in the View, but they will also update the Model when bound values in the View are changed by the user. This behavior could be triggered by form input or some other type of event, such as clicking a button.

There are some predefined two-way binders included with Rivets.js. As you might expect, it provides for the most common use case - a text input:

<input type="text" rv-value="dog.name"> 
 

Using the rv-value attribute to bind a Model property to an input element will prepopulate the value for that input with the bound Model value, and it will also update the Model value when the user changes the value of the input.

Defining your own two-way binder

To define a custom two-way binder in Rivets.js, a much more explicit approach must be taken, in contrast to one-way binders. This is because you must define how to bind and unbind to an element, as well as the data binding routine to run when the bound value changes:

rivets.binders.validate = { 
    bind: function(el) { 
        adapter = this.config.adapters[this.key.interface]; 
        model = this.model; 
        keypath = this.keypath; 
 
        this.callback = function() { 
            value = adapter.read(model, keypath); 
            adapter.publish(model, keypath, !value); 
        } 
 
        $(el).on('focus', this.callback); 
    }, 
 
    unbind: function(el) { 
        $(el).off('blur', this.callback); 
    }, 
 
    routine: function(el, value) { 
        $(el)[value ? 'removeClass' : 'addClass']('invalid'); 
    } 
}; 

Using the special property definitions shown in this example, we are telling Rivets.js to bind to an input onfocus and to unbind from the input onblur. Additionally, we define a routine to run when the value changes in which a className of invalid is added to the input when the value is empty, and removed when it is populated.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.80.209