📜 ⬆️ ⬇️

Ribs.js - nested attributes, computed fields and binding for Backbone.js



Hello! My name is Valery Zaitsev, I am a client side developer of the Target Mail.ru project. In our project, we use the well-known library Backbone.js, and, of course, we have something to miss. After thinking about possible solutions to our problems, I decided to write my addition to Backbone.js, as they say with blackjack and ... I want to tell you about it in this article.

Ribs.js is a library that enhances Backbone. And the beauty of what expands and does not change. You can use your favorite Backbone, as before, but by necessity use new features:

Consider these features in more detail.

Nested Attributes

Let's start with the simplest and most obvious. If you write a lot on Backbone, then you probably have a problem when you need to make changes to the model, the attributes of which are far from flat.
var Simpsons = Backbone.Ribs.Model.extend({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } } }); var family = new Simpsons(); 

Suppose Homer had a good lunch and gained a few kilograms:
')
Backbone:
 var homer = _.clone(family.get('homer')); homer.weight = 92; family.set('homer', homer); 

In order not to violate the get / set approach, we need to:
  1. pick up an object from the model;
  2. create a copy of this object;
  3. make the necessary changes;
  4. put back.

Agree, this is extremely inconvenient. And if you take into account the fact that objects can be huge, then it is also very expensive. It is much easier to change exactly the attribute you need:

Backbone + Ribs:
 family.set('homer.weight', 92); 

As a result of this set-a, the event 'change:homer.weight' will be generated. It is possible that you need events to be generated across the entire nesting chain. To do this, the set method must pass {propagation: true} .
 family.set('homer.weight', 92, {propagation: true}); 

In this case, the events 'change:homer.weight' and 'change:homer' will be generated.

Calculated Attributes

At once I will make a reservation that I used to call them computed fields, so please excuse me for double terminology. So let's get started. Very often there is a situation when the attributes of the model need to be transformed into a specific form (let's call it “result”), and then use this result, and not even in one place. And it would be nice if the attribute is updated, the result is updated, and everything that is tied to it would also be updated. The result is a rather cumbersome string of additional methods and subscriptions, which in the future will be quite problematic to maintain.

For example, Professor Frink conceived some kind of insane research in which it is very important for him to control the total weight of Homer and Bart. Let's compare implementations on pure Backbone and on Backbone + Ribs.

Backbone:
 var Simpsons = Backbone.Model.extend({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } } }); var family = new Simpsons(), doSmth = function (model, value) { console.log(value); }; family.on('change:bart', function (model, bart) { var prev = family.previous('bart').weight; if (bart.weight !== prev) { doSmth(family, bart.weight + family.get('homer').weight); } }); family.on('change:homer', function (model, homer) { var prev = family.previous('homer').weight; if (homer.weight !== prev) { doSmth(family, homer.weight + family.get('bart').weight); } }); var bart = _.clone(family.get('bart')); bart.weight = 32; family.set('bart', bart);//   : 122 var homer = _.clone(family.get('homer')); homer.weight = 91; family.set('homer', homer);//   : 123 

It was possible to write a little differently, but it does not greatly save the situation. Let us examine what we have written here. Defined a function that will do something with the desired total weight. Subscribed to handling 'change:homer' and 'change:bart' . In the handlers, we check if the weight value has changed, and in this case we call our working function. Agree, there is a lot of scribbling for a fairly simple and common situation. Now the same, but shorter, clearer and easier.

Backbone + Ribs:
 var Simpsons = Backbone.Ribs.Model.extend({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } }, computeds: { totalWeight: { deps: ['homer.weight', 'bart.weight'], get: function (h, b) { return h + b; } } } }); var family = new Simpsons(), doSmth = function (model, value) { console.log(value); }; family.on('change:totalWeight', doSmth); family.set('bart.weight', 32); //   : 122 family.set('homer.weight', 91); //   : 123 

What is going on here ?! We added a calculated field that depends on two attributes. When changing any of the attributes, the calculated field is recalculated automatically. The calculated attribute can be perceived as a normal attribute.

You can read its meaning:
 family.get('totalWeight'); // 120 

You can subscribe to change it:
 family.on('change:totalWeight', function () {}); 

If necessary, you can describe the set method for the calculated field, and set it without a twinge of conscience. It should be noted that the calculated fields can be used in the dependencies of other calculated fields. Also, computed fields are very convenient in binding!

Binding

Binding is the connection between a model and a DOM element. Easier here and not say. Every day a web developer has to output all kinds of data to the interface. Watch for their changes. Update. Output again ... And then the work day is over. Let's return to our yellow friends. Suppose we wanted to output the total weight in some span .

Backbone:
 var Simpsons = Backbone.Model.extend({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } } }); var Table = Backbone.View.extend({ initialize: function (family) { this.family = family; family.on('change:bart', function (model, bart) { var prev = this.family.previous('bart').weight; if (bart.weight !== prev) { this.onchangeTotalWeight(bart.weight + family.get('homer').weight); } }, this); family.on('change:homer', function (model, homer) { var prev = family.previous('homer').weight; if (homer.weight !== prev) { this.onchangeTotalWeight(homer.weight + family.get('bart').weight); } }, this); }, onchangeTotalWeight: function (totalWeight) { this.$('span').text(totalWeight); } }); var family = new Simpsons(), table = new Table(family); 


Backbone + Ribs:
 var Simpsons = Backbone.Ribs.Model.extend({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } }, computeds: { totalWeight: { deps: ['homer.weight', 'bart.weight'], get: function (h, b) { return h + b; } } } }); var Table = Backbone.Ribs.View.extend({ bindings: { 'span': {text: 'family.totalWeight'} }, initialize: function (family) { this.family = family; } }); var family = new Simpsons(), table = new Table(family); 

Now, with any changes in the weight of Homer or Bart, the span will be updated. In addition to the text, you can create other links between the parameters of the DOM elements and the attributes of the models:

In addition to the usual binding in Ribs.js, you can create a binding collection . The description of this mechanism deserves a separate article, therefore, within the framework of this article I will tell in two words. Binding a collection links a collection of models, Backbone.View, and a certain DOM element. For each model from the collection, its own instance of View is created and placed in the DOM element. And with any changes to the collection (adding / removing models, sorting) the interface is updated without your intervention. This gives you a dynamic presentation for the entire collection. The scope is obvious - various lists and structures with the same type of data.

Why choose Ribs.js and not something else?

On the Internet there are a number of libraries that add the ability to work with nested attributes. There are libraries that implement binding. But these are different libraries, and making them work together is a very difficult task, and most likely unrealizable.

The three components of Ribs.js (nested attributes, calculated fields, and binding) can work independently of each other. But all power is revealed when you use them together (the last example clearly illustrates this).

The closest competitor I know is Epoxy.js. This is a library with similar features, but:

Using Ribs.js, you can concentrate on business logic, without being distracted by the implementation of the simplest mechanisms. The code becomes clearer and more compact, and this has a very positive effect on both the development itself and subsequent support. In addition, work on Ribs.js will continue. Many ideas implemented in Ribs.js were born in the course of work on real combat missions. These ideas will appear further, and the best of them will fall into the next versions of the library.

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


All Articles