Alexander Beletsky's development blog

My profession is engineering

Introducing Backbone.ComputedFields

Recently, I’ve been working on small project which I want to share here. It’s called Backbone.ComputedFields and it’s small plugin that extends Backbone.Model functionality a bit.

I needed to have a model with ‘virtual’ fields. Namely, fields that does not belong to model directly, but being computed based on some other fields values.

The easiest solution would be simply introduce some model methods, say model.getComputedField() / model.setComputedField() and store the value inside the model object. But that turns out to be bad idea, for several reasons. First, we are breaking usual Backbone interface for getting and setting values - model.get('computed') / model.set('computed', 100). Also, if model is binded to a view, we are responsible for raising events manually, in case of computed or depended field changing.

So, after few iterations Backbone.ComputedFields was born. The design goals: to be simple, to be declarative, to be friendly to model binding (read, respect the events).

Use cases

Typical use cases for Backbone.ComputedFields are: calculating the prices; concatenating several fields; encapsulating the logic of retrieving object by reference.

It’s fairly important, that computed field could change. Based on it’s value, dependent fields should be updated.

Examples

Let’s take a look on few examples. The models here are very simplified. But, it shows the main application of Backbone.ComputedFields.

Calculating prices

The model represents the product, which contains net price and VAT rate.

var Produc = Backbone.Model.extend({
    initialize: function () {
        this.computedFields = new Backbone.ComputedFields(this);
    },

    computed: {
        grossPrice: {
            depends: ['netPrice', 'vatRate'],
            get: function (fields) {
                return fields.netPrice * (1 + fields.vatRate / 100);
            },
            set: function (value, fields) {
                fields.netPrice = value / (1 + fields.vatRate / 100);
            }
        }
    }
});

So, we have grossPrice as computed field. That field depends on ‘netPrice’ and ‘vatRate’ and being calculated by simple formulas.

var product = new Product({ netPrice: 100, vatRate: 20 });
var grossPrice = product.get('grossPrice');
    

In this case, gross price would be 120.

product.set({grossPrice: 300});
var netPrice = product.get('netPrice');

After gross price is update, netPrice will be recalculated and netPrice will be 250.

Concatenating fields

Let’s have a model to represent the person with first name and last name.

var Person = Backbone.Model.extend({
    initialize: function () {
        this.computedFields = new Backbone.ComputedFields(this);
    },

    computed: {
        fullName: {
            depends: ['firstName', 'lastName'],
            get: function (fields) {
                return fields.firstName + ' ' + fields.lastName;
            }
        }
    }
});

I’m skipping the setter cause we don’t need to set full name here.

var person = new Person({firstName: 'Alexander', lastName: 'Beletsky'});
var fullName = person.get('fullName');

Full name ‘Alexander Beletsky’ is returned here.

Referenced objects

Sometimes we have a models that only contains a reference to another model. All the time, we need to get referenced object we have to create some piece of code, which it typically copy-n-pasted thought the code base. Computed field could be a good idea to encapsulate that.

var Invoice = Backbone.Model.extend({
    initialize: function (attrs, options) {
        this.customers = options.customers;
        this.computedFields = new Backbone.ComputedFields(this);
    },

    computed: {
        customer: {
            depends: ['customerId'],
            get: function (fields) {
                return fields.customerId && this.customers.get(fields.customerId);
            },
            set: function(customer, fields) {
                fields.customerId = customer.get('id');
            }
        }
    }
});

Here we have customer field, which is computed.

var invoice = new Invoice({}, { customers: collectionOfCustomers });
var customer = invoice.get('customer');

So, I’m able to get customer model even if invoice is just holding the invoice Id.

invoice.set({customer: anotherCustomer});
var customerId = invoice.get('customerId');
    

If I’m changing the customer of invoice, the ‘customerId’ would be initialized with id of ‘anotherCustomer’.

Conclusions

Backbone.ComputedFields is still pre-mature, but I already successfully used that in one of projects. The github page contains pretty much documentation, so you should have to problems of adopting it for personal needs.