Get A Spine

From jQuery soup to reusable components

In the beginning

My current work is a large, old, public-facing web application, a dating site that has been hacked on by many people over many years. It's 300,000 lines of code. 50,000 lines of that are Javascript.

Some of it is really astonishingly bad.

<div class='profile oneProf bluebg'>
    <a href='#' onclick='popModal("somePage");return false'>Joe User</a>
    <div class='profimg onepic' id=profileimg1234 style='background-image:url(/image/profile1234.jpg)'></div>
    <p id=profile1235 class='profile oneprofile profileblk red newoldstyle'>18 | Versatile <br>Boston Massachusetts</p>
    <a id='profmaillink1235' class='button btn btnnew oneclickbutton'>Mail</a>
    <a id='profimlink1235' class='button btn btnnew oneclickbutton disabled'>IM</a>
    <script>
        jQuery('#profile1235').click(function() { popModal('profile1234') });
        jQuery('#profmaillink1235').click(function() { popMail('profile1234') });
        jQuery('#profimlink1235').click(function() { popChat('profile1234') });
    </script>
    </div>

That's a single profile on a dating site.

What went wrong

In a word: focus.

  • A new feature request? Add more styles.
    • btn
    • btn btnnew
    • btn btnnew profbutton linkbutton btnbluespecial2012
  • Design in the small: "make that blue darker"
  • Special cases: just do them inline.

Untangling the mess

We found some strategies for fixing it up, and some libraries that help a lot.

  • Make a wall. Clean inside it. Slowly extend the area.
  • Start small. Fix one page or section at a time.
  • Use a style metalanguage.
  • Attach event handlers to component roots.

Break things into pieces.

Make Components

  • Make identifiable things:
    • a user's profile block
    • the menu bar
    • window content frame
  • There's style, too, not just javascript or markup.
  • They're small. We have a dozen different components on a page. Some are a single button. Some are complicated (like a map or a whole menu bar)
  • Everyone thinks components are a good thing, and there's a lot of thinking going on about how to do them right

Backbone

Event handlers at component roots

var Profile = Backbone.View.extend({
    events: {
        'click .mail': 'openMail',
        'click .im': 'openChat'
    },
    openMail: function (ev) {
        if ($(ev.target).is('.disabled')) return false;
        popModal(this.attr('data-profile'));
    },
    ...
});

We can modify the contents of the elements without rebinding event handlers. They're all in predictable places. Getting event handling right is a great place to start cleaning up code.

Style metalanguages

Styles broken up by component root (We use LESS)


.Profile {
    .img {
        width: 50px;
        height: 50px;
    }
    a.disabled {
        color: #444;
    }

    background-color: #69e;
    display: inline-block;
}
                        

There's only one problem.


<link href='/components/window.css' rel='stylesheet'>
<link href='/components/profile.css' rel='stylesheet'>
<link href='/components/menu.css' rel='stylesheet'>
<link href='/components/searchbox.css' rel='stylesheet'>
<link href='/components/map.css' rel='stylesheet'>
<link href='/components/autocomplete.css' rel='stylesheet'>
<script src='/components/window.js'></script>
<script src='/components/profile.js'></script>
<script src='/components/menu.js'></script>
<script src='/components/searchbox.js'></script>
<script src='/components/map.js'></script>
<script src='/components/autocomplete.js'></script>
<script>
    var ourAppWindow = new AppWindow({el: $('body')});
</script>
                        

That's a lot of things to maintain on a page

The profile component

profile.js

define(['backbone'], function(Backbone) {
    return Backbone.View.extend({
        events: {
            'click .mail': 'openMail',
            'click .im': 'openChat'
        },
        openMail: function (ev) {
            if ($(ev.target').is('.disabled')) return false;
            popModal(this.attr('data-profile'));
        }
    });
});

profile.less

.Profile {
    .img {
        width: 50px;
        height: 50px;
    }
    a.disabled {
        color: #444;
    }

    background-color: #69e;
    display: inline-block;
}

The window component

window.js

define(['backbone', 'components/profile'], function(Backbone, Profile) {
    return Backbone.View.extend({
        initialize: function () {
            var views = this.views = [];
            this.$('.Profile').each(function() {
                views.push(new Profile({el: $(this)}); 
            });
        }
    });
});

window.less

@import url('./profile.less');

The page markup

<link href='/components/window.css' rel='stylesheet'>
<div class='Profile'>
    <div class='img' style='background-image: url(/i/12345.jpg)'>
    </div>
    <div class='info'>
        <div data-field='age'>18</div><br>
        <div data-field='location'>Boston, MA</div>
    </div>
    <div class='controls'>
        <a href='/mail?to=1235' class='mail'>Mail</a>
        <a href='/chat/1235' class='im'>IM</a>
    </div>
</div> <!-- Times a hundred! -->
<script>
require(['jquery', 'components/window'], function($, Window) {
    var ourAppWindow = new AppWindow($('body'));
});
</script>

A lot of the time, it's perfectly okay to render the document server-side

How do you do it?

Tips

  • Keep your views acyclic: References in one direction only. Propagate information back via a model.
  • Break it up. It's okay to have tiny views that do one thing well.
  • Communicate through models. Avoid making up new events.
  • Keep a separate model for the view's internal state, and update the main model from it on other actions

More examples

Radius control: the template is just an HTML5 range input. The view is trivial, and controls just a radius property in a model.

Map control: The view instantiates a Google Map. The inputs are lat and lon from a model — and a radius. The two views communicate only through that.

Colophon

I'm @aredridel (Aria Stewart), and I'm a Backboneaholic.

//aredridel.github.com/get-a-spine