Saturday, February 7, 2015

How to refactor spaghetti without getting depressed

using the Backbone.View.Elements library

It’s not yet another post about slick and sexy code architecture you can achieve using React, Angular or what’s now in vogue? The article is about situation when you have pile of jQuery spaghetti wrapped to Backbone views. Sounds familiar?

The problem #1: poorly expressive selectors

All of us know spaghetti in JavaScript is reflection of malformed HTML templates. Therefore, most likely such the code contains some tricky ambiguous DOM transformations and manipulations. It’s very hard to comprehend it because you need to keep in mind a lot of indistinct names of elements while you are trying to find out what’s going on in general. Let’s give expression to our code:
_selectors: function () {
    return {
        elemName: '.block__elem-name'
    };
}
Move selectors to one place and give understandable names to elements which will be retrieved by them. Now we can search elements of the view like this
this._elem('elemName');
instead of
this.$('.block__elem-name');
Not bad? Let’s go ahead!



In this particular case you can say the code became more expressive a bit, but do not forget we talking about a project with very cool selectors beyond the semantic like
div > tr.row[data-active="true"] a.red-Button
for “Buy” buttons.

Yet another advantage using Backbone.View.Elements is that you have to change the selector in the only one place in JavaScript if your HTML template is changed. Code duplication reduced. Cool.

The problem #2: elements storage

Sometimes you can see
$('div > tr.row[data-active="true"] a.red-Button').blahBlah();
and in 10 lines
$('div > tr.row[data-active="true"] a.red-Button').anotherBlahBlah();

You think “facepaaalm” and move the expression to a variable:
var $buyButton = $('div > tr.row[data-active="true"] a.red-Button');
no, you are using Backbone, you move it to a property
this._$buyButton = this.$('div > tr.row[data-active="true"] a.red-Button');
or you have already included Backbone.View.Elements?
this._$buyButton = this._elem('buyButton');

Actually you don’t need to keep it anywhere because _elem caches results. So just use
this._elem('buyButton');

It caches.. and what about invalidation?

Yeah, we also heard about the two hard problems. Therefore
this._findElem('elemName');
searches elements without using the cache
this._dropElemCache('elemName');
clears the cache for the particular element
this._dropElemCache();
drops all your cache when you realize the time is now. For example, after rendering.

Global things

Specially for you we wrapped to jQuery most often used elements and placed them to properties:
this._$window;
this._$body;
this._$document;

The problem #3: imperative styles

There is CSS, the whole language to describe styles, but we are talking about another surprising world, we are talking about spaghetti where styles are stuck in JavaScript creating one more barrier for code reading.
$('div > tr.row[data-active="true"] a.red-Button').css({color: 'magenta'});

Let’s declare styles inside stylesheets
.button_active {
    color: magenta;
}
and Backbone.View.Elements will care about classes manipulations. First, move all CSS classes to one method
_classes: function () {
    return {
        activeButton: 'button_active'
    };
}
then you will be able to add classes
this._addClass('activeButton', 'buyButton');
remove classes
this._removeClass('activeButton', 'buyButton');
and toggle classes
var condition = !!Math.round(Math.random());
this._toggleClass('activeButton', 'buyButton', condition);

You can also generate a selector by class name
this._selector('activeButton'); // returns '.button_active'
or search elements by this selector
this._elem('activeButton');
But do not forget about cache since the active button probably changes in time
this._findElem('activeButton');

The problem #4: when everything becomes difficult

From time to time we need to generate classes and selectors dynamically
var id = 5,
    state = 'highlighted';
$('.item[data-id="' + id + '"]').addClass('item_state_' + state);
and understanding becomes complicated dramatically. Welcome complex selectors!
_classes: function () {
    return {
        itemInState: 'item_state_%s'
    };
},

_selectors: function () {
    return {
        itemById: '.item[data-id=%s]'
    };
}
Hardly have you described them like this when the code below becomes true
this._class('itemInState', 'highlighted') === 'item_state_highlighted';
this._selector('itemInState', 'highlighted') === '.item_state_highlighted';
this._selector('itemById', 5) === '.item[data-id=5]';
And the manipulation above turns to
var id = 5,
    state = 'highlighted';
this._addClass(['itemInState', state], ['itemById', id]);
The class "item_state_highlighted" will be added to jQuery collection found by selector ".item[data-id=5]"

Extreme selectors complexity

You can use named placeholders for selectors and classes
_classes: function () {
    return {
        item: 'item_%(mod)s_%(value)s'
    };
}
And then
this._elem('item', {
    mod: 'state',
    value: 'focused'
});
will find jQuery collection by ".item_state_focused"

Problem #5: retrieving data

We’ve added some sugar for data attributes. All of them for the root element can be found inside the "_data" property. For example, if you have your view initialized on a div
<div data-some-ids="[5,6,7]"></div>
then
this._data['someIds']; // returns [5,6,7]

And if you have data for particular element of the view you can rely to
this._getElemData('elemName', 'someIds');
or for retriving whole data
this._getElemData('elemName'); // returns {someIds: [5,6,7]}

About inclusion and installation..

..and more documented API please read Readme.md on GitHub

You might also enjoy git diff of todomvc with Backbone.View.Elements and without it

Thanks for reading! Have a nice code :)

No comments:

Post a Comment