Lateralus is a JavaScript application framework that we’ve been developing here at Jellyvision. It’s not a from-scratch framework, and it’s not a hyper-ambitious framework meant to change how you think of web development. Instead, it’s a simple component layer on top of Backbone that we wrote to address some of the challenges we faced during the development of ALEX. Lateralus is designed to be abstract enough to be useful in any kind of web application. For example, I used it as the foundation of my recent rewrite of Stylie, an open source web animation tool.

Lateralus focuses on simplicity and expressiveness. It tries not to be too much, but there are quite a handful of micro-features that it provides that collectively serve to streamline the application development workflow. This article will focus on the three features of Lateralus that I like the most.

Automatic DOM binding

View logic has a tight relationship with the DOM. This relationship begins with the View querying the DOM for an element and caching it to prevent subsequent lookups. In a pure Backbone app, the HTML might look like:

<div class="my-view">
  <button class="action-button">Action!</button>
</div>

With the following View code:

var MyView = Backbone.View.extend({
  initialize: function () {
    this.$actionButton = this.$('.action-button');
  }
});


var myView = new MyView({ el: document.querySelector('.my-view') });

The MyView instance now has a reference to .action-button. This “wiring up” would have to be repeated for every DOM element that the View needs to work with. This can get quite unwieldy, so Lateralus provides a simpler approach from the DOM:

<!-- app/scripts/components/my-component/template.mustache -->
<button class="$actionButton">Action!</button>

To the view:

// app/scripts/components/my-component/view.js
define(['lateralus', 'text!./template.mustache'], 
    function (Lateralus, template) {

  var MyComponentView = Lateralus.Component.View.extend({
    template: template
  });
});

At this point, you might think “wait, that looks completely different. Where is the DOM element being bound?” The answer is that isn’t, at least not explicitly. Lateralus does the DOM binding automatically. That means that if you want to access the button element in the view, you can just do something like this (notice the new logButtonText method):

// app/scripts/components/my-component/view.js
define(['lateralus', 'text!./template.mustache'], 
    function (Lateralus, template) {

  var MyComponentView = Lateralus.Component.View.extend({
    template: template,

    logButtonText: function () {
      console.log(this.$actionButton.text());
    }

  });
});

Because there is a leading $ in the name of the first class of the button, Lateralus was able to bind that as a corresponding property in the View. This automatic binding helps to significantly cut down on the amount of boilerplate code needed to set up the relationship between a View and the DOM. If you need to set up the DOM bindings again, you can explicitly call bindToDOM at any time.

Render data pipeline

In addition to building a relationship between the DOM and a View, it is frequently necessary to populate a template with data. Lateralus makes this easy with the getTemplateRenderData View method. This method gets called internally just before Lateralus renders a View’s template and passes it to Mustache.render. This makes it easy to pipe along data to a template:

<!-- app/scripts/components/my-component/template.mustache -->
<button class="$actionButton">{{buttonText}}</button>

From a view:

// app/scripts/components/my-component/view.js
define(['lateralus', 'text!./template.mustache'], 
    function (Lateralus, template) {

  var MyComponentView = Lateralus.Component.View.extend({
    template: template,

    getTemplateRenderData: function () {
      return {
        buttonText: 'This is my awesome button!'
      };
    }
  });
});

The end result would be:

<button class="$actionButton">This is my awesome button!</button>

…Which you can still access as this.$actionButton. If at any point you want to manually empty the View’s root element and re-render it with the data returned by getTemplateRenderData, you can do so by calling renderTemplate.

Component communication

So far we’ve covered the ways that Lateralus makes it easy to wire up the static components of a UI, but that’s only a small part of application development. The real challenge comes in the form of inter-component communication. This is usually the hardest thing to manage as an application grows. However, the trick to scaling an application is actually quite simple:

“The secret to building large apps is NEVER build large apps. Break up your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application.”

http://bitovi.com/blog/2010/11/organizing-a-jquery-application.html

How to break an application down into components is a blog post in itself, and this React tutorial does a great job of explaining it from a high level. Lateralus provides some simple yet powerful APIs for allowing components to broadcast and respond to messages. All of these APIs leverage the incredibly well-done Backbone.Events API and use the central Lateralus instance as the message bus for an application. There are two primary APIs for working with messages:

  • emit: Broadcasts a message to the entire Lateralus application. Any component can listen for the emitted message.
  • lateralusEvents: A map that specifies a set of emitted messages to listen for and the handlers to respond to them with.

These APIs are available to all classes that use the Lateralus.mixins methods. Specifically, these are:

The general flow is that one object will emit a message, and another object will respond to it as specified by its lateralusEvents map. A basic example template:

<!-- app/scripts/components/button-component/template.mustache -->
<button>Click me!</button>

With a view:

// app/scripts/components/button-component/view.js
define(['lateralus', 'text!./template.mustache'], 
    function (Lateralus, template) {

  var ButtonView = Lateralus.Component.View.extend({
    template: template,

    events: {
      'click button': function () {
        this.emit('buttonClicked');
      }
    }
  });
});

In another component’s template:

<!-- app/scripts/components/header-component/template.mustache -->
<h1 class="$header"></h1>

With a view:

// app/scripts/components/header-component/view.js
define(['lateralus', 'text!./template.mustache'], 
    function (Lateralus, template) {

  var HeaderView = Lateralus.Component.View.extend({
    template: template,

    lateralusEvents: {
      buttonClicked: function () {
        this.$header.text('The button component was clicked!');
      }
    }
  });
});

In this case, HeaderView will display a message when the button in ButtonView is clicked. However, each View has no awareness of the other. These means that the two objects are loosely coupled, and one can be removed or changed without breaking the other. Because of the undirected broadcast nature of messages, there is a one-to-many relationship: You can have any number of message responders for a single emitter.

This decoupling allows for massive application scale. Complexity become unmanageable when components have a tight dependence on another, and eliminating that dependence allows you to focus on the isolated implementation details of each component. This has enabled us to cut down significantly on the complexity of the applications that Jellyvision builds.

This is not a revolution

Our goal with Lateralus was not to rethink how applications are built, it was to solve the problems we were experiencing. So far, the framework has helped us do that. A little bit of workflow and architecture optimization go a long way! Hopefully you’ll find this project helpful as well. We look forward to working with the community to improve Lateralus and make it even better. Spiral out!




Tagged with: